Skip to content

Breakpoints

Five names, four widths, and one place they are written down. Everything in the library that changes with the width of the window reads the same ladder, including your md: utilities, which is the point.

The ladder

RungFromNotes
xs0Everything is at or above it. It has no floor, which is why it is never a bound.
sm40rem · 640px
md48rem · 768px
lg64rem · 1024px
xl80rem · 1280px

They are the same five names as size, deliberately. A reader who has learned the ladder once should not have to learn a second set of words for where a screen changes shape. They are not the same ladder, a size is how tall a control is and a breakpoint is how wide the window is, but they run in the same direction and turn up in the same sentence.

The widths are Tailwind's own, so a layout decided by a md: utility and a layout decided by this library change at the same moment. A page with two answers about how wide it is is a page that drifts by a few pixels for no reason anybody can find later.

Moving them

One line, in your own Tailwind theme:

css
@import 'tailwindcss';
@import 'plass-ui/tailwind.css';

@theme {
  --breakpoint-md: 50rem;
}

Both halves of the library follow it, PlGrid's cascade, PlContainer's measure, PlShow, usePlBreakpoint, and a PlSidebar's collapseBelow.

There is no provider prop for this, and there cannot be. A media query's condition cannot read a custom property: @media (width >= var(--x)) is not valid CSS and never will be. So a breakpoint the stylesheet decides at is resolved when the stylesheet is compiled, not when a component renders, the library writes its own with @variant, which is Tailwind's, which is your theme's.

The JavaScript half is the other kind of question. There a breakpoint is a value rather than a condition, so it can be read off the document, and the stylesheet publishes it as four tokens for exactly that:

css
--plass-breakpoint-sm: var(--breakpoint-sm, 40rem);
--plass-breakpoint-md: var(--breakpoint-md, 48rem);
--plass-breakpoint-lg: var(--breakpoint-lg, 64rem);
--plass-breakpoint-xl: var(--breakpoint-xl, 80rem);

Set the Tailwind variable, not these. Setting one of these moves the JavaScript half on its own, which leaves the two disagreeing. The exact failure the arrangement exists to prevent.

On the precompiled path this is baked. A project that imports plass-ui/styles.css gets a stylesheet we compiled, with our widths in it; there is no Tailwind on that side to re-run. Import plass-ui/tailwind.css instead if you need to move a breakpoint.

The ladder is PlassBreakpoint, and its widths are the React package's, which are Tailwind's. There is no stylesheet here and so nothing to compile against: the widths are fixed.

Responsive values

A value that changes with the width is written the same way everywhere in the library. A bare value applies at every width; a map applies each entry from its own breakpoint up.The base value applies from zero up and every override applies from its own breakpoint up.

tsx
<PlGridItem span={6} />                    // six columns at every width
<PlGridItem span={{ xs: 12, md: 6 }} />    // full on a phone, half from 48rem

There is no xs fallback to write out: an entry cascades to the widths above it, which is what keeps a responsive prop to the breakpoints it actually names. { lg: 3 } names one rung, not five.

dart
PlGridItem(span: const PlassResponsive<int>(6));               // six everywhere
PlGridItem(span: const PlassResponsive<int>(12, md: 6));       // full, then half

Dart has no untagged union, so the base value is the first positional argument and the overrides are named. PlassResponsive(6) is the whole of "six columns everywhere".

Which props take one:

ComponentProp
PlGridcolumns spacing rowSpacing columnSpacing
PlGridItemspan offset
PlContainer PlHeader PlFootermaxWidth
PlPanes PlTabs PlScrollZone PlTimeline PlStepperorientation
PlStackdirection

Resolving a responsive value

This is the part worth understanding, because it decides what a responsive prop costs and what it can do.

A value that decides only style is resolved in CSS. The component writes one --p-* custom property per rung the caller named and the stylesheet cascades it down from the rung above. Nothing measures anything, nothing re-renders when a window is dragged, and the first paint a server sends is already correct at every width, because the browser is what resolves it. PlGrid and PlContainer work this way, and PlShow is the same idea with display.

A value that decides structure cannot be. An orientation changes which DOM a component builds, which ARIA it claims and which way its arrow keys go, and no stylesheet can do that. Those are resolved by usePlBreakpointValue, which is JavaScript, so a server renders the xs entry and the browser corrects it on hydration.

The library reaches for the CSS half whenever it can, and you should too.

Everything is resolved in the widget, against MediaQuery.sizeOf(context).width, and it is correct on the first frame. There is no server render to disagree with, so the split the React build has to make does not arise here. The breakpoint is the window's width rather than the widget's own box, which is what a media query measures and what makes two widgets side by side agree about which rung they are on however wide each of them ended up.

Showing one thing or another

PlShow is the gate: from, until, or both as a band.

It hides with display: none, so both halves are in the document and neither is read out twice. When only one of them should exist at all (it fetches, it is expensive, it holds state) use usePlBreakpointValue, and mount one.

Asking in JavaScript

Three hooks, for the decisions CSS cannot make, how many items to fetch, which of two components to mount, how many characters to truncate at.

All three answer false / xs on a server, and that is not a bug to work around: it is what makes the markup a server sends deterministic. Anything that has to be right in the first frame belongs in CSS, a Tailwind variant, or PlShow. These are for what comes after.

PlassBreakpoint.of(MediaQuery.sizeOf(context).width) is the whole of it, and PlassResponsive.resolve(breakpoint) reads a responsive value against it.

PlSidebar's own breakpoint

PlSidebar collapses into a drawer below collapseBelow, and it is worth reading as the worked example of everything above: the decision is a media query in CSS for the first paint and matchMedia in JavaScript from then on, because the markup a server sends is the column and a phone must not draw one and throw it away.

Released under the MIT License