fdb/docs

[§] /docs/rules/breakpoints-and-layout

Breakpoints and layout

Five named breakpoints, three containers, gutters and grid columns, proximity, and the 390px rule.

TL;DR

  • Mobile first. Use Tailwind's breakpoint names as they are: sm md lg xl 2xl.
  • Three containers: prose 65ch, page 1200px, wide 1440px.
  • Gutters of 16px or more on phones. Grid columns use minmax(0, 1fr).
  • Gaps between groups are about twice the gaps inside them.
  • Every page works at 390px and 320px with no sideways scroll.

Design mobile first and add layout as the screen grows. Use Tailwind's five breakpoint names and values unchanged, so every developer and every AI agent already knows them.

Breakpoints

Breakpoints: table (6 rows)
NameMin widthTypical deviceTailwind
(base)0Phones, portraitno prefix
sm640pxLarge phones landscape, small tabletssm:
md768pxTablets portraitmd:
lg1024pxTablets landscape, small laptopslg:
xl1280pxLaptops, desktopsxl:
2xl1536pxLarge desktops2xl:
  • MUST write base styles for the phone and add min-width overrides upwards. No max-width media queries except for a one-off fix.
  • MUST keep breakpoints in tokens (--breakpoint-md: 48rem in @theme). Never write @media (min-width: 812px) by hand.
  • SHOULD change layout at no more than 3 breakpoints per page. Most pages need only md and lg.
  • SHOULD use container queries for components that appear in different column widths. Tailwind v4 has them built in: @container on the parent, @md:grid-cols-2 on the child.
Media query or container query?

if it changes the page shell (nav, sidebar, columns)

→ media query: md:, lg:

if it is a card or widget reused in sidebars and main columns

→ container query: @container, @md:

if it only needs to wrap

→ neither: flex-wrap or auto-fit grid

if it depends on hover or pointer

→ @media (hover: hover) and (pointer: fine)

Rule of thumb: pages respond to the viewport, components respond to the space they are given.

Test widths

Check every page at these widths before calling it done.

Test widths: table (7 rows)
WidthWhy
320pxThe reflow width in WCAG 2.2 (1.4.10). Content must work without two-way scrolling
390pxThe standard modern iPhone width. The house rule below
768pxTablet portrait, the first layout change
1024pxTablet landscape, where sidebars usually appear
1280pxCommon laptop
1440pxCommon desktop. Check the container stops growing
1920pxFull HD. Check nothing stretches edge to edge by accident

Containers

Three widths cover almost every page.

Containers: table (3 rows)
TokenValueUse
--container-prose65ch (about 680px at 16px)Articles, docs, long text
--container-page1200pxStandard page content
--container-wide1440pxDashboards, galleries, wide tables
Containers: copy the css (12 lines)
@theme {
  --container-prose: 65ch;
  --container-page: 1200px;
  --container-wide: 1440px;
}

.container-page {
  width: 100%;
  max-width: var(--container-page);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
  • MUST cap reading text at 65ch, even inside a wider container.
  • MUST let backgrounds go full bleed and keep content inside the container. The section is wide; the text is not.
  • SHOULD NOT add a fourth container width. Use a grid span instead.

Gutters and grid columns

The grid follows Material's responsive layout grid: 4 columns on phones, 8 on tablets, 12 on desktop.

base · 4 cols · gap 16

md · 8 cols · gap 24

lg · 12 cols · gap 32

main · span 8
aside · 4
The same page on three grids. Phones get 4 columns and 16px gutters, tablets 8 columns and 24px, desktops 12 columns and 32px. Content spans whole columns, never fractions.
Gutters and grid columns: table (3 rows)
BreakpointColumnsGutter (side padding)Column gapSection spacing
base (0 to 639px)416px16px64px
sm / md (640 to 1023px)824px24px80px
lg and up (1024px+)1232px32px96px
Gutters and grid columns: copy the css (13 lines)
:root {
  --gutter: 16px;
  --grid-cols: 4;
  --grid-gap: 16px;
}
@media (min-width: 40rem) { :root { --gutter: 24px; --grid-cols: 8;  --grid-gap: 24px; } }
@media (min-width: 64rem) { :root { --gutter: 32px; --grid-cols: 12; --grid-gap: 32px; } }

.grid-page {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  gap: var(--grid-gap);
}
  • MUST keep side gutters at 16px or more on phones. Text touching the screen edge is the most common phone bug.
  • MUST use minmax(0, 1fr), not 1fr, for grid columns that hold text or code, so long content cannot force the column wider.
  • SHOULD use 12 columns on desktop because it divides into halves, thirds, quarters and sixths.
  • SHOULD span whole columns (col-span-8, col-span-4), never set widths like width: 63%.
  • SHOULD use repeat(auto-fit, minmax(16rem, 1fr)) for card grids that should not need breakpoints at all.

Safe areas and viewport height

Phones have notches, rounded corners and home indicators. Opt in to the full screen, then pad away from the unsafe edges.

Safe areas and viewport height: copy the tsx (8 lines)
// app/layout.tsx (Next.js)
import type { Viewport } from 'next';

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  viewportFit: 'cover',
};
Safe areas and viewport height: copy the css (14 lines)
.site-header {
  padding-top: env(safe-area-inset-top);
  padding-inline: max(var(--gutter), env(safe-area-inset-left)) max(var(--gutter), env(safe-area-inset-right));
}

.bottom-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
}

.hero { min-height: 100svh; }   /* small viewport: never jumps when the toolbar hides */
.app-shell { height: 100dvh; }  /* dynamic viewport: tracks the toolbar */
  • MUST add env(safe-area-inset-bottom) to anything fixed to the bottom of the screen once viewport-fit=cover is set.
  • MUST use max() so the gutter is never smaller than the normal 16px when the inset is 0.
  • MUST NOT use 100vh for full-height layouts on phones. It is taller than the visible area while the browser toolbar shows. Use 100svh for heroes and 100dvh for app shells.
  • MUST NOT use 100vw for full-bleed sections. It includes the scrollbar width on desktop and causes a horizontal scroll.

The 390px rule

Every page MUST work at 390px wide with no horizontal scroll. This is the house rule from the rules. Test it in the browser's device mode, then run this in the console:

[...document.querySelectorAll('*')].filter((el) => el.getBoundingClientRect().right > innerWidth + 1);

An empty array passes. Anything listed is wider than the screen. The usual causes and fixes:

The 390px rule: table (8 rows)
CauseFix
Fixed widths (width: 480px)max-width: 100% or a grid span
Long URLs, emails, IDsoverflow-wrap: anywhere on the text container
Flex children that refuse to shrinkmin-width: 0 (min-w-0) on the child
Wide tables and code blocksWrap in a container with overflow-x: auto; scroll the block, not the page
100vw sectionswidth: 100%
Three columns with no base stylegrid-cols-1 md:grid-cols-3
Images and embeds without limitsmax-width: 100%; height: auto
Negative margins for bleed effectsPut overflow-x: clip on the section wrapper
  • MUST check at 390px and at 320px, which is the WCAG reflow width.
  • MUST keep inputs at 16px text so iOS does not zoom and shift the layout on focus.
  • SHOULD use overflow-x: clip rather than hidden on wrappers, because clip does not create a scroll container and does not break position: sticky.

Group by proximity

✕ equal gaps

✓ grouped

Left: groups separated by 6px, the same as the gap inside them, so the eye can't find the structure. Right: 32px between groups, so there are clearly two ideas.

Related things sit close. Unrelated things sit far apart. The gap between groups should be about twice the gap within them. This does more for structure than any border or card.

  • SHOULD allow asymmetry. A 1:2 split feels more designed than three equal columns.
  • SHOULD give major sections 80 to 120px of vertical space on desktop.
Columns
Header

Layout on a grid

Lines land on the 8px rhythm.

Image
Intro text
Card 1
Card 2
Card 3

grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 8px;

Every block starts and ends on a column line, and every line of text sits on an 8px baseline. Switch to 4 columns (a phone) and the same layout reflows by spanning whole columns, never arbitrary widths.

Turn the overlay on. Every block lines up with a column edge and every line of text with the 8px baseline. Drop to 4 columns and the layout reflows by spanning whole columns.

[ex] in the wild · 12

Why it works

Shared breakpoint names mean "it breaks at md" is a complete bug report. A small, fixed set of containers and a column grid make alignment automatic, so edges line up across sections without anyone measuring. The 390px check catches the one failure that makes a site feel broken on the device most people will actually use.

Sources

3 sources

On this page