Breakpoints
Every example on this page is live, drawn by the library's Storybook, where you can also change its props and see the result.
Breakpoints let you lay your widget out with Tailwind classes that follow the size of the
widget's own box, so one tree of elements rearranges itself for every shape a widget can take on
a frame. They are container queries, not the sm: and md: prefixes you may know from web
pages: those follow the page, while a widget has to follow its box, wherever it is shown.
There is nothing to import. Write a breakpoint in front of any Tailwind utility, and the utility applies only while the box is in that range:
| Breakpoint | Applies while the box is... | Preview Sizes it catches |
|---|---|---|
@sm: | too wide to be a slim column | Square, Brick, Tall, Wide, Banner, Full |
@md: | at least half a frame wide | Wide, Banner, Full |
@lg: | wide enough for a block to one side | Wide, Banner, Full |
@max-sm: / @max-md: / @max-lg: | narrower than that | Column for @max-sm:, Square, Brick and Tall for @max-md: |
short: | too low for more than one row, and at least half a frame wide | Banner |
compact: | too low for more than one row, and too short for a strip | Brick |
tall: | high enough for a roomier layout | Tall, Column, Full |
No Preview Size lands between @md: and @lg:, but a widget can be dragged to any size in the
Studio and placed in any slot on a frame, and @md: is there for the boxes in between: wide
enough for two blocks side by side, not yet for a panel beside the main one.
The widths and heights these turn on are the same ones useLayoutShape uses to name a box's shape, so a class and the hook always agree:
| Shape | In classes |
|---|---|
band | short: |
compact | compact: |
narrow | @max-sm: (a box at least as high as a square slot) |
side | @lg: (a box that is not also short) |
stack | everything else, so the classes with no prefix |
tall | tall: |
A box that is too low for more than one row but too short to be a strip is compact, even when
it is smaller than a square slot, and compact: applies to it rather than short:. Any other box smaller than
a square slot in both directions is a stack, so neither short: nor compact: applies to it.
It can still be @max-sm:, since that is a width alone: use @max-sm: for what a slim box needs
whatever its height, such as stacking an icon over a figure, and leave the column's own
arrangement to the plan from the hook.
Which one wins
Write the classes for the smallest box first, with no prefix, and add the larger shapes in front
of the utilities that change. A box can match a width and a height breakpoint at once: a Banner
is both @lg: and short:. The height breakpoints come later in the stylesheet, so when both
set the same property, the height one wins. That is usually what you want, since a strip has no
room for the side-by-side layout a wide box gets. To ask for both at once, stack them
(@lg:tall:gap-6), and to rule one out, negate it (not-short:flex-col).
Classes for the frame, the hook for the props
Every widget starts from useLayoutShape: it names the box's shape, and the props a class
cannot change follow from it, such as a component's size, a Divider's orientation or how
many days a DayStrip shows. Breakpoints take the rest, everything CSS can set: which way a
row runs, how blocks align, the gaps between them, how wide a side panel is, and what is
hidden in a small box. You can also pick the whole frame in code from the shape and use the
breakpoints only for the parts a class carries well, such as hiding a block in a small box;
either way the two agree, since they turn on the same numbers. Here the frame is classes:
import { useLayoutShape } from "~lib/use-layout-shape";
import { Display, Heading } from "~lib/typography";
import { Icon } from "~lib/icon";
import { Divider } from "~lib/layout/divider";
import { KeyValue } from "~lib/data-display/key-value";
import { DayStrip } from "~lib/data-display/day-strip";
export const Widget = () => {
const { shape, tall } = useLayoutShape();
const beside = shape === "band" || shape === "side";
const forecast = shape !== "compact";
return (
<div className="flex size-full flex-col gap-3 overflow-hidden @lg:flex-row @lg:gap-6 short:flex-row short:items-center short:gap-3">
<div className="flex min-h-0 min-w-0 flex-1 flex-col justify-center gap-3 short:flex-none">
<Heading level={tall ? 2 : 4}>Today</Heading>
<div className="flex items-center gap-3 @max-sm:flex-col">
<Icon name="cloud-sun" size={tall ? 64 : 40} />
<Display size={tall ? "lg" : "sm"} unit="°C">
18
</Display>
</div>
<div className="hidden tall:block">
<KeyValue
orientation="horizontal"
size="sm"
data={[
{ key: "Feels like", value: "17°C" },
{ key: "Humidity", value: "64%" },
]}
/>
</div>
</div>
{forecast && (
<>
<Divider
orientation={beside ? "vertical" : "horizontal"}
spacing={beside ? "sm" : "none"}
/>
<div className="flex min-w-0 flex-col justify-center @lg:w-[42%] short:w-auto short:flex-1">
<DayStrip
direction={shape === "side" || tall ? "column" : "row"}
size={tall ? "md" : "sm"}
days={[
{ date: "2026-10-06", primary: "20° / 9°" },
{ date: "2026-10-07", primary: "15° / 10°" },
{ date: "2026-10-08", primary: "14° / 7°" },
]}
/>
</div>
</>
)}
</div>
);
};
Reading the root from left to right: a column with a small gap, which becomes a row with a
larger gap once the box is wide enough for a side panel, and a tighter row centered on its
height when the box is a strip. The main block takes the room left in either direction and gives
it up in a strip, the details only appear in a tall box, the icon stacks above the figure in a
column too narrow for both, and a compact box shows today alone. The divider and the forecast are
left out in code rather than hidden with compact:hidden, since a Divider takes no classes.
Recipes
- The root is
flex size-full flex-col gap-3 overflow-hidden, then@lg:flex-rowfor a wide box andshort:flex-row short:items-centerfor a strip. Give it no padding: the person who installs the widget sets the space around it, so padding of your own would double up with theirs. - A side panel is
@lg:w-[42%]on a block that is otherwise in the column's flow; the main block beside it ismin-w-0 flex-1. - A block that takes the rest is
min-h-0 min-w-0 flex-1, so it can shrink whichever way the row runs, andshort:flex-nonewhere a strip wants it sized by its content. - A header beside a strip's row is
short:w-44 short:shrink-0; the row takes the rest. - Less in a small box is
hidden tall:blockon something only a tall box has room for,compact:hiddenon what a compact box has no room for, and@max-sm:flex-colon a row that has to stack in a narrow column. - A grid that can take another column is
grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3.
What to avoid
sm:,md:andlg:without the@. They follow the page, not the widget. On a device a widget renders in a page of its own, so they seem to work in the Studio and then follow the wrong box in the widget catalog, where the same markup is shown inside the console.- Your own
@containerclass. The widget's root is already the container the breakpoints measure; an element you make a container of its own becomes what its descendants measure instead, and its breakpoints stop following the widget's box. - Your own width and height checks in code. The breakpoints and
useLayoutShapeturn on the same numbers, which the Preview Sizes are tuned to; a threshold of your own would not be.
Every Preview Size
The sample widget above, drawn at each Preview Size. In this storybook the classes carry the
library's lib: prefix; in a widget they are written as in the code above.
Any Size
See Also
- useLayoutShape - The box's shape and the room inside it, for the props a class cannot set
- useResolution - The exact size of the widget's box
- Card - A bordered block for a grid of items
- Divider - A rule between blocks, horizontal or vertical