# Custom Breakpoints When Needed

**SHOULD** · **ID:** `design-custom-breakpoints` · **Category:** design
**Source:** [Tailwind](https://tailwindcss.com/docs)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-custom-breakpoints

> SHOULD: Add custom breakpoints via theme.extend.screens; avoid arbitrary min-[Xpx]: values

Add project-specific breakpoints as --breakpoint-* theme tokens, not as scattered arbitrary min-[…] values

The v3 answer, `theme.extend.screens`, no longer exists — there is no config object to extend. A breakpoint in v4 is just another theme token: declare `--breakpoint-3xl: 120rem;` inside `@theme` and you get `3xl:` (plus `max-3xl:` and every other variant that keys off it) for free. The rule underneath is unchanged and is the reason this principle exists: a raw `min-[960px]:` is a magic number with no name, so the second person who needs that breakpoint guesses 961 or 959 and the layout now breaks in a one-pixel band. Name it once, use the name everywhere. Two v4 specifics worth knowing: because breakpoints are ordinary custom properties, they are sorted by value rather than by declaration order, so a new token slots into the right place in the cascade automatically; and `--breakpoint-*: initial;` wipes the built-in scale if you want only your own names.

## Bad — do not do this

`design-custom-breakpoints-bad`

```tsx
import { useState } from 'react';

const rules = [
  { cls: 'min-[1919px]:hidden', at: 1919 },
  { cls: 'min-[1920px]:flex', at: 1920 },
  { cls: 'min-[1921px]:grid', at: 1921 },
];

export function CustomBreakpointsBad() {
  const [w, setW] = useState(1920);
  const activeCount = rules.filter((r) => w >= r.at).length;
  const inconsistent = activeCount > 0 && activeCount < rules.length;

  return (
    <div className="w-full max-w-md space-y-4">
      <div className="rounded-lg border border-border bg-card p-4">
        <div className="mb-3 flex items-center justify-between">
          <h4 className="font-medium">Drag the viewport width</h4>
          <span
            className={`rounded px-1.5 py-0.5 font-mono text-xs ${
              inconsistent ? 'bg-error/15 text-error' : 'bg-muted text-muted-foreground'
            }`}
          >
            {w}px
          </span>
        </div>

        <input
          type="range"
          min={1916}
          max={1924}
          value={w}
          onChange={(e) => setW(Number(e.target.value))}
          aria-label="Viewport width"
          className="w-full accent-primary"
        />

        <div className="mt-3 space-y-1.5">
          {rules.map((r) => {
            const on = w >= r.at;
            return (
              <div
                key={r.cls}
                className={`flex items-center justify-between rounded-md border px-2 py-1 font-mono text-xs ${
                  on ? 'border-error/40 bg-error/10 text-error' : 'border-border text-muted-foreground'
                }`}
              >
                <span>{r.cls}</span>
                <span>{on ? 'on' : 'off'}</span>
              </div>
            );
          })}
        </div>

        <div
          className={`mt-3 rounded-md p-2 text-xs ${
            inconsistent
              ? 'border border-error/30 bg-error/10 text-error'
              : 'bg-muted text-muted-foreground'
          }`}
        >
          {inconsistent
            ? `${activeCount} of 3 rules fire — the switch that should happen at one width is smeared across this 1919–1921px band. The layout is in a state nobody designed.`
            : 'Here all three happen to agree — until the next person nudges one number and reopens the gap.'}
        </div>
      </div>

      <p className="text-xs text-error">
        Three inlined magic numbers, three thresholds. A single named token would switch at exactly one width;
        these drift into an off-by-one dead band no one intended.
      </p>
    </div>
  );
}
```

## Good — do this

`design-custom-breakpoints-good`

```tsx
import { useState } from 'react';

const breakpoints = [
  { name: 'sm', px: 640, cols: 1 },
  { name: 'md', px: 768, cols: 2 },
  { name: 'lg', px: 1024, cols: 2 },
  { name: 'xl', px: 1280, cols: 3 },
  { name: '2xl', px: 1536, cols: 3 },
  { name: '3xl', px: 1920, cols: 4, custom: true },
];

export function CustomBreakpointsGood() {
  const [active, setActive] = useState(5);
  const bp = breakpoints[active];

  return (
    <div className="w-full max-w-md space-y-4">
      <div className="rounded-lg border border-border bg-card p-4">
        <div className="mb-3 flex items-center justify-between">
          <h4 className="font-medium">Resize the viewport</h4>
          <span className="font-mono text-xs text-muted-foreground">≥ {bp.px}px</span>
        </div>

        <div className="mb-3 flex flex-wrap gap-1">
          {breakpoints.map((b, i) => (
            <button
              key={b.name}
              onClick={() => setActive(i)}
              className={`flex items-center gap-1 rounded-md px-2.5 py-1 font-mono text-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring ${
                i === active
                  ? 'bg-primary text-primary-foreground'
                  : 'bg-muted text-muted-foreground hover:text-foreground'
              }`}
            >
              {b.name}
              {b.custom && (
                <span
                  className={`rounded-sm px-1 text-[9px] ${
                    i === active ? 'bg-primary-foreground/20' : 'bg-primary/15 text-primary'
                  }`}
                >
                  custom
                </span>
              )}
            </button>
          ))}
        </div>

        <div className="rounded-md border border-border bg-muted/40 p-2">
          <div
            className="grid gap-1.5"
            style={{ gridTemplateColumns: `repeat(${bp.cols}, minmax(0, 1fr))` }}
          >
            {Array.from({ length: bp.cols * 2 }).map((_, k) => (
              <div key={k} className="h-8 rounded bg-primary/15" />
            ))}
          </div>
        </div>

        <div className="mt-3 rounded-md bg-muted p-2 font-mono text-xs">
          <span className="text-muted-foreground">applies </span>
          <span className="text-foreground">
            {bp.name}:grid-cols-{bp.cols}
          </span>
        </div>
      </div>

      <p className="text-xs text-success">
        The custom <code>3xl</code> token behaves like every built-in breakpoint — one name, sorted into the
        scale by its value, and the width is written down exactly once.
      </p>
    </div>
  );
}
```

## References

- [Tailwind v4: Responsive design](https://tailwindcss.com/docs/responsive-design)
- [Tailwind v4: Theme variables](https://tailwindcss.com/docs/theme)
