Display and sizing
The low-level half of layout: what box an element makes, how it aligns its children, how big it is allowed to get, and what happens when its content does not fit. Most pages want stack, cluster or grid instead — those are the same properties with the decisions already made. These are for the cases the primitives do not cover.
Display
.contents is the interesting one. It removes the element's own box
while keeping its children in the parent's layout, which is how you get a
server-rendered wrapper out of the way of a grid without changing the HTML. It
also removes the box from the accessibility tree in some engines, so do not put it
on anything with a role or a label.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.hidden |
display: none
|
— | 09-utilities.css:10 |
.block |
display: block
|
— | 09-utilities.css:11 |
.inline-block |
display: inline-block
|
— | 09-utilities.css:12 |
.flex |
display: flex
|
— | 09-utilities.css:13 |
.inline-flex |
display: inline-flex
|
— | 09-utilities.css:14 |
.contents |
display: contents
|
— | 09-utilities.css:15 |
Flex direction and alignment
.grow sets flex: 1 1 auto and
min-inline-size: 0 together. The second half is the fix for the
commonest flex bug there is: a flex item will not shrink below its content, so a
long unbroken string in a .grow item pushes the row wider than its
container and the whole page picks up a horizontal scrollbar.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.flex-row |
flex-direction: row
|
— | 09-utilities.css:18 |
.flex-col |
flex-direction: column
|
— | 09-utilities.css:19 |
.wrap |
flex-wrap: wrap
|
— | 09-utilities.css:20 |
.nowrap |
flex-wrap: nowrap; white-space: nowrap
|
— | 09-utilities.css:21 |
.grow |
flex: 1 1 auto; min-inline-size: 0
|
— | 09-utilities.css:32 |
.shrink-0 |
flex: 0 0 auto
|
— | 09-utilities.css:33 |
.items-start |
align-items: flex-start
|
— | 09-utilities.css:22 |
.items-center |
align-items: center
|
— | 09-utilities.css:23 |
.items-end |
align-items: flex-end
|
— | 09-utilities.css:24 |
.items-baseline |
align-items: baseline
|
— | 09-utilities.css:26 |
.items-stretch |
align-items: stretch
|
— | 09-utilities.css:25 |
.justify-start |
justify-content: flex-start
|
— | 09-utilities.css:27 |
.justify-center |
justify-content: center
|
— | 09-utilities.css:28 |
.justify-end |
justify-content: flex-end
|
— | 09-utilities.css:29 |
.justify-between |
justify-content: space-between
|
— | 09-utilities.css:30 |
.place-center |
place-items: center
|
— | 09-utilities.css:31 |
Size
All of these are logical: .w-full sets
inline-size and .h-full sets block-size, so
they follow the writing mode rather than the screen.
.min-h-screen uses dvh rather than vh, which
is what stops a full-height layout from being cropped by the address bar on a
phone.
.min-is-0 is .grow's second half on its own, for grid
items — a grid track's minimum is auto too, so a wide table inside one
drags the whole track past the viewport unless the item is allowed to shrink.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.w-full |
inline-size: 100%
|
— | 09-utilities.css:76 |
.w-auto |
inline-size: auto
|
— | 09-utilities.css:77 |
.h-full |
block-size: 100%
|
— | 09-utilities.css:78 |
.min-h-screen |
min-block-size: 100dvh
|
— | 09-utilities.css:79 |
.min-is-0 |
min-inline-size: 0
|
— | 19-logical.css:74 |
.square |
aspect-ratio: 1
|
— | 09-utilities.css:84 |
.max-w-sm |
max-inline-size: 24rem
|
— | 09-utilities.css:81 |
.max-w-md |
max-inline-size: 34rem
|
— | 09-utilities.css:82 |
.max-w-lg |
max-inline-size: 48rem
|
— | 09-utilities.css:83 |
.max-w-prose |
max-inline-size: var(--measure)
|
max-inline-size: 68ch
|
09-utilities.css:80 |
Aspect ratio
Three ratios with object-fit: cover already set, for the media slot in
a card or a gallery. Setting the ratio in CSS rather than leaving the image to size
itself is what stops the page reflowing as images arrive.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.ratio-16x9 |
aspect-ratio: 16 / 9; object-fit: cover; inline-size: 100%
|
— | 04-layout.css:229 |
.ratio-4x3 |
aspect-ratio: 4 / 3; object-fit: cover; inline-size: 100%
|
— | 04-layout.css:230 |
.ratio-1x1 |
aspect-ratio: 1; object-fit: cover; inline-size: 100%
|
— | 04-layout.css:231 |
.square |
aspect-ratio: 1
|
— | 09-utilities.css:84 |
Overflow
.overflow-clip and .overflow-hidden both cut the content
off; the difference is that hidden makes the element a scroll
container and clip does not. That matters more than it sounds:
overflow: hidden on an ancestor is what breaks
position: sticky, and swapping it for clip usually fixes
a sticky header that mysteriously does not stick.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.overflow-auto |
overflow: auto
|
— | 09-utilities.css:173 |
.overflow-clip |
overflow: clip
|
— | 09-utilities.css:174 |
.overflow-hidden |
overflow: hidden
|
— | 09-utilities.css:172 |
Visibility and input
.hidden removes an element from the page and from assistive
technology. .sr-only does the opposite: it keeps the element in the
accessibility tree and takes it off the screen, which is how you label an icon-only
control. They are not interchangeable, and using .hidden for a label
is how a button ends up announced as "button".
.touch-only and .mouse-only key off
pointer: coarse and pointer: fine rather than width, so a
touchscreen laptop is treated by what it has rather than by how wide it is. Neither
is a substitute for making a control work with both.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.sr-only |
position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0
|
— | 09-utilities.css:182 |
.skip-link |
position: absolute; inset-block-start: -100%; inset-inline-start: var(--space-4); z-index: var(--z-toast); background: var(--brand-600); color: var(--text-on-brand); padding: var(--space-3) var(--space-4); border-radius: var(--r-sm)
|
position: absolute; inset-block-start: -100%; inset-inline-start: 1rem; z-index: 500; background: oklch(51.6% var(--chroma-brand) var(--hue-brand)); color: oklch(99% .01 var(--hue-brand)); padding: .75rem 1rem; border-radius: 7px
|
09-utilities.css:193 |
.touch-only |
display: none
|
— | 09-utilities.css:238 |
.mouse-only |
display: none
|
— | 09-utilities.css:239 |
.cursor-pointer |
cursor: pointer
|
— | 09-utilities.css:179 |
.no-select |
user-select: none
|
— | 09-utilities.css:178 |
.pointer-none |
pointer-events: none
|
— | 09-utilities.css:177 |
.opacity-60 |
opacity: .6
|
— | 09-utilities.css:176 |
Breakpoint prefixes
Deck has three viewport breakpoints — 40rem, 48rem and 64rem — and a small set of prefixed classes that turn on at each. They are mobile-first: the prefixed class applies at that width and above, so the unprefixed state is the phone.
<div class="stack stack-3 md:row md:items-center">
<img class="ratio-1x1" src="…" alt="">
<p>Stacked on a phone, side by side from 48rem up.</p>
</div>
The available combinations are fixed rather than generated, so this is not a system where any class can take any prefix. The rows below list what exists at each breakpoint, taken from the source.
The three rows below are named .sm, .md and
.lg because that is how they land in the generated inventory: the
extractor reads a class name up to the first character that cannot be in one, and
in sm\:hidden that is the backslash escaping the colon. The classes
you write are the full prefixed names. Recorded so the table is not silently
wrong about it.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.sm |
.sm\:hidden@media (min-width: 40rem)display: none
|
— | 09-utilities.css:207 |
.md |
.md\:hidden@media (min-width: 48rem)display: none
|
— | 09-utilities.css:215 |
.lg |
.lg\:hidden@media (min-width: 64rem)display: none
|
— | 09-utilities.css:229 |
| Breakpoint | Applies from | Classes |
|---|---|---|
sm: |
40rem |
hidden block flex row w-auto text-start |
md: |
48rem |
hidden block flex grid-d row col items-center justify-between w-auto text-center p-8 gap-6 |
lg: |
64rem |
hidden block flex grid-d row gap-8 |
Container queries
A viewport breakpoint asks how wide the window is. A container query asks how wide the element's own column is, which is nearly always the question you actually meant: the same card should stack in a sidebar and spread out in a main column, and the window has no opinion about which of those it is in.
.cq marks an element as a container. The named variants exist so a
deeply nested component can query a specific ancestor instead of the closest one —
without a name, @container always resolves to the nearest container,
which is rarely the one you meant once containers nest.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.cq |
container-type: inline-size
|
— | 04-layout.css:234 |
.cq-size |
container-type: size
|
— | 18-container.css:16 |
.cq-normal |
container-type: normal
|
— | 18-container.css:17 |
.cq-panel |
container: panel / inline-size
|
— | 18-container.css:19 |
.cq-pane |
container: pane / inline-size
|
— | 18-container.css:20 |
.cq-row |
container: row / inline-size
|
— | 18-container.css:21 |
.cq-shell |
container: shell / inline-size
|
— | 18-container.css:22 |
.cq-tone |
container-type: inline-size
|
— | 18-container.css:182 |
Container breakpoint prefixes
The same idea as md:, keyed to the container at 24rem, 32rem and
44rem. These only do anything inside an element carrying .cq, and the
same extractor caveat applies to their names.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.cq-sm |
.cq-sm\:row@container (min-width: 24rem)flex-direction: row
|
— | 18-container.css:153 |
.cq-md |
.cq-md\:row@container (min-width: 32rem)flex-direction: row
|
— | 18-container.css:160 |
.cq-lg |
.cq-lg\:row@container (min-width: 44rem)flex-direction: row
|
— | 18-container.css:170 |
Container-driven patterns
Five ready-made ones. .metarow is a list row that reveals metadata as
space allows: .row-meta appears at 26rem of container width and
.row-meta-wide at 38rem, so the same row is terse in a narrow pane and
complete in a wide one with no JavaScript measuring anything.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.actions-cq |
display: flex; flex-direction: column; gap: var(--space-2)
|
display: flex; flex-direction: column; gap: .5rem
|
18-container.css:89 |
.metarow |
display: flex; align-items: center; gap: var(--space-3)
|
display: flex; align-items: center; gap: .75rem
|
18-container.css:70 |
.row-meta |
.metarow .row-metadisplay: none
|
— | 18-container.css:71 |
.row-meta-wide |
.metarow .row-meta-wide@container (min-width: 38rem)display: block
|
— | 18-container.css:76 |
.stat-cq |
display: flex; flex-direction: column; gap: var(--space-1)
|
display: flex; flex-direction: column; gap: .25rem
|
18-container.css:55 |
Mirroring
Deck is written in logical properties, so a right-to-left page mirrors on its own.
These two are for the exceptions. .mirror-rtl flips an element
horizontally when the direction is RTL — for a directional glyph in an icon set
that does not have a mirrored variant. .no-flip pins something that
would otherwise be flipped by a rule higher up: a logo, a play button, a chart
axis.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.mirror-rtl |
[dir="rtl"] .mirror-rtlscale: -1 1
|
— | 19-logical.css:133 |
.no-flip |
[dir="rtl"] .no-flipscale: 1 1
|
— | 19-logical.css:150 |
Containment
Six classes that tell the engine it may skip work. .defer sets
content-visibility: auto, so an offscreen section is not laid out or
painted until it approaches the viewport; .virtual applies the same
idea per child, which is what makes a very long list scroll smoothly with no
virtualisation library.
The catch with .defer is that skipped content is skipped by in-page
find as well in some engines, and the contain-intrinsic-size estimate
is what keeps the scrollbar from jumping as sections resolve. Keep the estimate
close to the real height.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.contain |
contain: layout style
|
— | 26-perf.css:66 |
.contain-paint |
contain: layout style paint
|
— | 26-perf.css:67 |
.defer |
content-visibility: auto; contain-intrinsic-size: auto 600px
|
— | 26-perf.css:48 |
.virtual |
.virtual > *content-visibility: auto; contain-intrinsic-size: auto var(--item-size, 120px)
|
— | 26-perf.css:40 |
.virtual-sm |
.virtual-sm > *--item-size: 64px
|
— | 26-perf.css:44 |
.virtual-lg |
.virtual-lg > *--item-size: 220px
|
— | 26-perf.css:45 |
When not to use these
-
Not to rebuild a primitive.
.flex .flex-col .gap-4is.stackwritten out..flex .wrap .items-center .gap-3is.cluster. Use the name; it is one class and it carries the default. -
Not
.hiddenfor a visual-only hide. It takes the element out of the accessibility tree too. If it should be read but not seen, that is.sr-only. -
Not a breakpoint prefix where a container query fits. A
component that reflows on window width is wrong the moment somebody puts it in a
sidebar.
.cqpluscq-md:asks the right question. -
Not
.cursor-pointeron a<div>to make it a button. The cursor is the least of what a button is. Use a<button>and get the focus ring, the keyboard activation and the role for free. -
Not
.deferon everything. It costs a layout pass when the element comes into view. On short content it is slower than doing nothing.