Foundations

Breakpoints

Five Tailwind defaults, unchanged. Mobile-first. The viewport thresholds every layout, every grid, every padding ramp scales through.

Documentedby Derek Fidler

Overview

Flatpay uses Tailwind v4's default breakpoints verbatim — sm 640, md 768, lg 1024, xl 1280, 2xl 1536. No custom thresholds, no semantic aliases, no fork. The CSS preset and the legacy v3 config both leave --breakpoint-* untouched, so utilities behave identically across consumers.

The scale exists to handle two questions: when does the layout change shape? and by how much does a spacing/typography ramp step up?. Everything else is the component's problem, not the breakpoint scale's.

Active right now

Drag the browser narrower and wider — the chip matching the current viewport fills in. Pure CSS; no JavaScript, no resize listener.

<sm<640sm≥640md≥768lg≥1024xl≥12802xl≥1536

The scale

Five steps. sm through lg carry almost all the weight in product UI; xl and 2xl are present for completeness and used sparingly.

TokenTailwindrempxTypical use
  • breakpoint.smsm:40rem640Large phone landscape. Step up gaps and convert single-column lists to two-column grids.
  • breakpoint.mdmd:48rem768Tablet portrait. The tablet ↔ desktop hinge for product UI — most layout switches land here.
  • breakpoint.lglg:64rem1024Laptop. The hinge marketing pages use to introduce side-by-side hero / sidebar layouts.
  • breakpoint.xlxl:80rem1280Desktop. Rare. Reach for it only when the lg layout genuinely needs more room.
  • breakpoint.2xl2xl:96rem1536Ultrawide. Almost never. Cap the page with `max-w-*` instead of styling at this tier.

The values match `em`, not `px`

Tailwind v4 emits its media queries in em so user font-size preferences scale the layout too. The pixel values in the table are the threshold at the browser default of 16 px; if a user bumps their root size, the breakpoints move with them. Reach for the utility, not the pixel.

Mobile-first is the rule

Base classes target the narrowest viewport. Breakpoint-prefixed classes only ever step up — adding columns, growing padding, revealing chrome. The codebase is 100% mobile-first today; that consistency is doing real work.

Why mobile-first matters

Desktop-first classes (text-base md:text-sm) force the small-screen reader through a recomputation on every page load — they see the desktop value, then it shrinks. Mobile-first (text-sm md:text-base) lands on the right answer first try, every time.

Picking the right one

A simple mental model:

  • sm:Large phone in landscape. Step a stacked list into a two-column grid; widen a tight padding ramp. If a layout still feels cramped on a 380 px phone, fix the base — don't wait for sm.
  • md:Tablet portrait. The most useful step in the scale — product UI usually pivots from single-column to side-by-side here, and page gutters open up from 24 to 32 px.
  • lg:Laptop. Marketing pages switch to side-by-side hero / sidebar layouts; docs reveal a persistent table of contents.
  • xl:Desktop. Rare. Reach for it only when the lg layout genuinely needs more room — most of the time the answer is a wider max-w, not another breakpoint.
  • 2xl:Ultrawide. Almost never. If you're reaching for 2xl to cap a page, cap with max-w-* instead. The scale should not pay rent for a viewport you never styled for.

Design QA widths are not breakpoints

The design system asks for visual review at three widths — 375 (mobile), 768 (tablet), and 1440 (desktop). Those are test viewports, not utility thresholds. Only one of the three (768) lines up with a Tailwind breakpoint. The other two sit just inside the nearest tier: 375 is well below sm, 1440 is just below xl.

Design QA · vs · Tailwind utilities

Only one tick lines up — 768 px / md. Everything else sits just inside or just outside the nearest utility tier, so the QA widths exist to test the layout, not to drive it.

375Mobile
768Tablet
1440Desktop
sm640
md768
lg1024
xl1280
2xl1536

Use QA widths to test, not to style

Build mobile-first using the utility breakpoints, then resize the browser to 375 / 768 / 1440 to confirm the design holds. The QA widths are checkpoints. The utility scale is the language.

Component-level responsiveness

When a component needs to respond to its ownwidth — not the page width — reach for a container query. A card that lives in both a sidebar slot and a full-width slot can't use md: to decide its layout; the page is wide either way.

The Table component is the canonical example — it switches between dense and stacked layouts based on @container rather than the viewport. Mirror that pattern when a component must work inside an unpredictable slot.

In code

The patterns that recur across @flatpay-dk/ui and the docs site:

tsx

// Grid columns — single column on phone, three columns from sm up
<ul className="grid grid-cols-1 gap-3 sm:grid-cols-3">…</ul>

// Padding ramp — comfortable on phone, more breathing room as the viewport opens
<section className="px-6 py-12 md:px-12 md:py-16 lg:py-24">…</section>

// Conditional chrome — hide a persistent sidebar on small screens
<aside className="hidden lg:block">…</aside>

// Reading column — cap the body, scale the gutters mobile-first
<main className="mx-auto w-full max-w-3xl px-6 py-12 md:px-12 md:py-16">…</main>

// Stack → row at the tablet hinge
<div className="flex flex-col gap-4 md:flex-row md:items-center md:gap-8">…</div>

Don'ts

  • Don't hardcode pixel widths in media queries. Use the Tailwind utilities so v3 and v4 consumers stay in lockstep, and so user font-size preferences scale the layout naturally.
  • Don't write desktop-first responsive classes. text-base md:text-sm is backwards. Start at the smallest viewport and step up.
  • Don't invent new breakpoint names. If a layout breaks between md and lg, fix the layout, not the scale. Custom breakpoints fork the system.
  • Don't reach for 2xl: to cap a page. That's what max-w-* is for. A breakpoint is a layout switch, not a width ceiling.
  • Don't use viewport breakpoints when the component might sit in a narrow slot. A card in a 320 px sidebar doesn't care that the viewport is 1440. Use @container instead.