Skip to content

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.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

The ladder
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.

Container query, not media query

This card rearranges based on its own width. Drag the stage narrower — the viewport has not changed, but the card has.

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.

This heading scales continuously

clamp(1.25rem, 4cqw + 0.5rem, 2.5rem)

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.

(pointer: coarse)

Touch. Targets grow to 44px, hover affordances are removed.

(hover: hover)

A real pointer exists. Tooltips and hover previews are safe.

(prefers-reduced-motion)

Collapse animation durations.

(prefers-color-scheme)

The initial theme, before the user overrides it.

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.

4 col
base≥0px
8 col
sm≥640px
8 col
md≥768px
12 col
lg≥1024px
12 col
xl≥1280px
12 col
2xl≥1536px
@container
containercomponent-scoped
44px
coarsepointer type

Anatomy

Every part, every measurement, and the reason it is that number.

base
sm
md
lg
xl
2xl
0640102415361920

Min-width only, so each range inherits everything from the one below it. There is no max-width query anywhere in the system.

  1. Base0px, unqualified

    Mobile is not a breakpoint — it is the default. Everything else is progressive enhancement on top of a layout that already works.

  2. sm — 640px40rem

    Where two form fields can sit side by side at a comfortable measure, and where a dialog can stop being full-screen.

  3. 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.

  4. xl — 1280px80rem

    Enough room for a third region. The on-this-page rail in this Bible appears here and not before.

  5. 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.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Spacing

TokenValueUsed for
smTwo-up layouts, non-fullscreen dialogs
mdTables regain columns, secondary nav appears
lgPermanent sidebar, full application shell
xlRight-hand rail, master–detail
2xlContainer reaches its cap and centres
gutterPage padding, steps at sm
touch-targetMinimum on pointer: coarse

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizePaddingMin widthWhen to use
base24px0pxOne column. Bottom nav. Full-screen dialogs. Tables as cards.
sm32px640pxTwo-up cards. Paired form fields. Centred dialogs.
md32px768pxReal tables. Persistent secondary navigation.
lg40px1024pxPermanent sidebar. Three-up grids. Desktop density available.
xl40px1280pxRight rail. Master–detail. Four-up grids.
2xl40px1536pxContent caps at 76rem. Nothing new appears.

Do

@media (min-width: 1024px)@media (max-width: 1023px)
Write min-width queries onlyMobile-first means every rule adds rather than overrides. Mixing min and max produces overlapping ranges and bugs that exist at exactly one width.
.host { container-type: inline-size }
@container (min-width: 24rem) { … }
Reach for container queries firstA component that adapts to its own slot works in a sidebar, in a modal, in a two-column layout and in a full-width page — with one rule instead of four.
font-size: clamp(1.5rem, 2vw + 1rem, 2.5rem)
Use clamp() instead of stepped typeContinuous interpolation has no jump and no gaps. Three breakpoints for a heading size means three sizes and infinitely many widths where none of them is right.
@media (pointer: coarse) { .btn { min-block-size: 44px } }
Query capability for capabilityTouch targets depend on the pointer, not the window. A 1024px tablet needs 44px targets; a 700px browser window on a desktop does not.

Don't

@media (min-width: 375px) /* iPhone SE */@media (min-width: 390px) /* iPhone 14 */@media (min-width: 393px) /* Pixel 7 */@media (min-width: 430px) /* 14 Pro Max */
Do not add a breakpoint per deviceDevice dimensions change every year and never covered the real distribution anyway. Add a breakpoint when the content breaks, and only then.
.advanced-filters { display: none } — the feature no longer exists on mobile
Do not hide content on small screensIf it matters on desktop it matters on a phone. Hiding it says the mobile user deserves less product — and it is the most common cause of "I can only do this on my laptop".
.card { @media (min-width: 1024px) { flex-direction: row } }
Do not put media queries inside reusable componentsThe component now depends on where the page thinks it is rather than on the space it actually has. Drop it into a sidebar and it lays out for a viewport it cannot see.
if (width < 768) enableTouchMode() — wrong on every tablet ever made
Do not equate small screen with touchA 900px browser window on a desktop is a mouse. A 1180px iPad is a finger. Screen width has never been a reliable proxy for input method.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.4.10ReflowAA1.3.4OrientationAA1.4.4Resize TextAA

Contrast

  • Contrast requirements do not change with viewport, but text often gets smaller on mobile — verify at the smallest size you actually ship.

Keyboard

TabFocus 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".
AttributeApplied toNotes
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.
orientationMedia queryContent 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:noneResponsive hidingBoth remove content from assistive tech. If it should stay available, move it rather than hiding it.

Code

Example usage

tsx
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

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); }
}

Notes

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.