Breakpoints
Five thresholds, chosen from where layouts actually break rather than from a list of phone models. Plus the rule that most components should not use them at all.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
base≥0px
Phone
Single column. Bottom navigation. Sidebar becomes a drawer. Tables become cards.
sm≥640px
Large phone / small tablet
Two-up cards. Form fields can pair up. Dialogs stop being full-screen.
md≥768px
Tablet
Persistent secondary navigation. Tables regain their columns.
lg≥1024px
Laptop
Full application shell. Sidebar is permanent. Three-up card grids.
xl≥1280px
Desktop
The right-hand rail appears. Master–detail becomes viable.
2xl≥1536px
Wide
Container caps at 76rem and centres. Nothing else changes.
Container queries
Use the width controls in the toolbar. The card below responds to its own box, so it stays correct inside a sidebar, a modal, or a full-width page.
Fluid instead of stepped
clamp() interpolates continuously between a minimum and a maximum. No breakpoint, no jump, correct at every width including the ones you did not test.
Capability, not size
Touch targets and hover affordances should depend on the pointer, not the viewport. A 1024px-wide tablet is touch; a 700px-wide split-screen laptop is not.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Every part, every measurement, and the reason it is that number.
Min-width only, so each range inherits everything from the one below it. There is no max-width query anywhere in the system.
- Base0px, unqualified
Mobile is not a breakpoint — it is the default. Everything else is progressive enhancement on top of a layout that already works.
- sm — 640px40rem
Where two form fields can sit side by side at a comfortable measure, and where a dialog can stop being full-screen.
- lg — 1024px64rem
The most consequential one. A 268px sidebar plus a 68ch content column plus gutters needs about 1024px; below it, the sidebar must collapse.
- xl — 1280px80rem
Enough room for a third region. The on-this-page rail in this Bible appears here and not before.
- Container queries@container (min-width: …)
The default tool for components. Viewport breakpoints are reserved for the shell, which is the only thing that genuinely knows about the viewport.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Spacing
| Token | Value | Used for |
|---|---|---|
| sm | Two-up layouts, non-fullscreen dialogs | |
| md | Tables regain columns, secondary nav appears | |
| lg | Permanent sidebar, full application shell | |
| xl | Right-hand rail, master–detail | |
| 2xl | Container reaches its cap and centres | |
| gutter | Page padding, steps at sm | |
| touch-target | Minimum on pointer: coarse |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Padding | Min width | When to use |
|---|---|---|---|
| base | 24px | 0px | One column. Bottom nav. Full-screen dialogs. Tables as cards. |
| sm | 32px | 640px | Two-up cards. Paired form fields. Centred dialogs. |
| md | 32px | 768px | Real tables. Persistent secondary navigation. |
| lg | 40px | 1024px | Permanent sidebar. Three-up grids. Desktop density available. |
| xl | 40px | 1280px | Right rail. Master–detail. Four-up grids. |
| 2xl | 40px | 1536px | Content caps at 76rem. Nothing new appears. |
.host { container-type: inline-size }
@container (min-width: 24rem) { … }font-size: clamp(1.5rem, 2vw + 1rem, 2.5rem)@media (pointer: coarse) { .btn { min-block-size: 44px } }.card { @media (min-width: 1024px) { flex-direction: row } }Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- Contrast requirements do not change with viewport, but text often gets smaller on mobile — verify at the smallest size you actually ship.
Keyboard
| Tab | Focus order must stay sensible at every breakpoint. If a layout reorders visually, the DOM must reorder with it. |
| ⌘ / Ctrl + | At 400% zoom a 1280px window behaves like a 320px one. That is the actual WCAG reflow test. |
Screen readers
- Screen-reader users on a desktop may be at a large viewport with 400% zoom. Do not assume "large viewport" means "everything fits".
- Content hidden with display: none at one breakpoint is gone for everyone at that breakpoint, including assistive tech.
Focus & touch
- Focus must stay visible after a breakpoint change. If the focused element moves into a collapsed region, move focus explicitly rather than letting it fall to <body>.
- Reflow must work at 320 CSS pixels wide with no horizontal scrolling — equivalent to 1280px at 400% zoom. That is the requirement, not "looks fine on my phone".
| Attribute | Applied to | Notes |
|---|---|---|
| meta viewport | <head> | width=device-width, initial-scale=1. Never add maximum-scale or user-scalable=no — both block pinch zoom and fail WCAG 1.4.4. |
| orientation | Media query | Content must work in both orientations. Locking to one fails WCAG 1.3.4 unless the orientation is essential, which it almost never is. |
| hidden vs display:none | Responsive hiding | Both remove content from assistive tech. If it should stay available, move it rather than hiding it. |
Example usage
1// Mobile-first: the base case has no prefix2<div className="flex flex-col gap-4 lg:flex-row lg:gap-6">34// Reveal a region only when there is room for it5<aside className="hidden xl:block w-52">6 <TableOfContents />7</aside>89// Container queries for anything reusable10<div style={{ containerType: 'inline-size' }}>11 <article className="flex flex-col @[24rem]:flex-row">…</article>12</div>1314// Reading a breakpoint in JS — only when layout cannot express it15const isDesktop = useMediaQuery('(min-width: 1024px)')16return isDesktop ? <DataTable /> : <CardList />1718// Capability, not size19const coarse = useMediaQuery('(pointer: coarse)')2021// The single most important line in a responsive app22<meta name="viewport" content="width=device-width, initial-scale=1" />CSS
/* Min-width only. Each step adds; none of them override. */
@media (min-width: 40rem) { /* sm 640 */ }
@media (min-width: 48rem) { /* md 768 */ }
@media (min-width: 64rem) { /* lg 1024 */ }
@media (min-width: 80rem) { /* xl 1280 */ }
@media (min-width: 96rem) { /* 2xl 1536 */ }
/* Breakpoints in rem, so they respect the user's font size.
A user at 24px root font hits "lg" at a smaller pixel width —
which is correct, because their content is bigger. */
/* Components query themselves */
.card-host { container-type: inline-size; container-name: card; }
@container card (min-width: 24rem) {
.card { grid-template-columns: 6rem 1fr; }
}
/* Fluid beats stepped wherever the property is continuous */
.hero { font-size: clamp(2rem, 5vw + 1rem, 4rem); }
.page { padding-inline: clamp(1.5rem, 4vw, 2.5rem); }
/* Capability queries */
@media (pointer: coarse) {
.btn { min-block-size: 44px; }
}
@media (hover: hover) {
.row:hover { background: var(--ds-layer-hover); }
}Professional tips
- Define breakpoints in rem rather than px. A user with a 24px root font hits each threshold at a smaller pixel width, which is exactly right — their content is physically larger.
- Test at 320px. It is the narrowest viewport WCAG requires and the width a 1280px window becomes at 400% zoom.
- When you find yourself wanting a sixth breakpoint, you almost certainly want a container query on the component that is misbehaving.
- Landscape phones are wider than portrait tablets. Never assume a wide viewport means a large device.
Performance
- Media queries are free — they are evaluated during style resolution with no JavaScript involved. matchMedia in React costs a listener and a re-render per change.
- Do not render both a mobile and a desktop tree and hide one. You pay for both in DOM size, both in data fetching, and both in hydration.
- container-type: inline-size creates a containment context, which usually improves layout performance by bounding the recalculation.
- Responsive images with srcset and sizes prevent a phone downloading a 2400px hero. This is typically the single largest mobile performance win available.
Common mistakes
- Adding maximum-scale=1 or user-scalable=no to the viewport meta tag. It blocks pinch zoom and is a direct WCAG failure.
- Using vh for full-height layouts on mobile, where the browser chrome collapses and the value is wrong for most of the scroll.
- Hiding the primary navigation on desktop behind a hamburger. Space exists; use it.
- Testing only at the exact breakpoint values. Bugs live between them, at 900px and 1150px.
Real-world recommendations
- Check your analytics before choosing thresholds. Most products discover a large cluster around 1366×768 that their 1440px-first designs never considered.
- Build the 320px layout first. Everything is a decision at that width, and the desktop layout falls out of it almost for free. The reverse never works.
- A resizable panel in a desktop app is a viewport that media queries know nothing about. Any component that can live inside one needs container queries.
- Keep a page in the app that renders your key components at every breakpoint simultaneously in iframes. It catches regressions no single-width review ever will.