# Yeti 7.0.0-alpha.0
Yeti is a plain-CSS framework. Three rules cover every component: identity is a class (`class="card"`), configuration is a `data-*` attribute with a fixed value list (`data-gap="md"`), and state is native or ARIA (`open`, `aria-current`), never a class. Layouts arrange, recipes compose layouts, components are styled things. Every attribute below is validated: a value not in its list is an error. Rendered pages are checked with the same manifest by bin/validate-html.js in the Yeti repository (node bin/validate-html.js
--manifest ); run it over built HTML before looking for a layout fault by hand. Docs: https://foundationcss.com/yeti/
To begin a page, start from dist/starter/index.html, a complete and valid page (skip link, sticky nav, hero, card grid, prose, footer), and dist/starter/theme.css, the essential theme settings with their defaults, commented out. Copy both and delete what the page does not need.
## box (layout, class="box")
Pads its content on all sides, with an optional border and an optional surface.
Attributes:
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Padding on every side.
- data-gap-inline: none | xs | sm | md | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Padding on the two sides only, over whatever data-gap set: a band wide at the sides and ordinary top and bottom.
- data-gap-block: none | xs | sm | md | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Padding top and bottom only, over whatever data-gap set.
- data-surface: base | raised | sunken — Fill the box with one of the three surface tones, making it a visible panel. Absent, the box is transparent and only its padding does anything.
On children:
- data-border on * (boolean) — Draw a border of the border width, in the border color. Works on any element; it is a box's most common marker, which is why it is listed here.
- data-paint on *: primary | secondary | success | warning | alert | neutral | white | black | grey | grey-0 | grey-10 | grey-20 | grey-30 | grey-40 | grey-50 | grey-60 | grey-70 | grey-80 | grey-90 | grey-100 — Fill this element with a color by name: a hue at its base step with the text made for it, a constant, or a step of the greyscale. Up to grey-40 the text stays the page text; from grey-50 it becomes the page surface.
- data-text on *: primary | secondary | success | warning | alert | neutral | white | black | grey | grey-0 | grey-10 | grey-20 | grey-30 | grey-40 | grey-50 | grey-60 | grey-70 | grey-80 | grey-90 | grey-100 — Color this element's words by name, from the same list. On an element that is also painted it wins over the automatic text color.
### When to use it
A box is the thing to reach for when content needs breathing room from its edges: a card body, a callout, a panel in a sidebar. Combine it with a stack inside for the spacing between its children, and with `data-surface` or `data-border` to make it something you can see.
### How it works
`padding` on all four sides from `data-gap`, a border of `--yeti-border-width` in `--yeti-color-border` when `data-border` is present (the marker works on any element, not only a box), and a fill from `data-surface`. Nothing else. Unlike the spacing layouts, a box does not reset its children's margins, so paragraphs inside it keep their prose rhythm.
`data-surface` takes the three surface tones. `raised` is a step above the page and is what a panel usually wants; `sunken` is a step below, for a well that content sits in; `base` is the page's own surface, for a panel inside a sunken area that should not change tone. Without it a box is transparent and only its padding does anything, which is right when the thing behind it is already a surface.
`data-gap-inline` and `data-gap-block` pad one axis over whatever `data-gap` set, so a band that is wide at the sides and ordinary top and bottom is `data-gap-inline="xl"` and nothing else. They take the same words as `data-gap`, fluid pairs included. This is the whole of Yeti's padding utility: any element becomes a box with the class, and the box says how much.
There is no corner radius, on purpose. A box is square. A rounded panel that lifts off the page is a `card`, and the two should not blur into each other.
```html
Title
Body
```
### Painting anything
`data-surface` names a role of the page: raised, sunken, or the page itself. A color by name is `data-paint`, and any element can carry it, not only a box: a hue at its base step, one of three constants, or a step of a greyscale that flips with the scheme, with `data-text` for the words alone. The [color guide](guides/color.md) has the list, the rule for the text on each, and what a grey step means.
### Why this name
There is no plainer word for a padded rectangle. Foundation 6's Callout was a styled box with a color scheme; the plain one had no name.
Tokens:
- --yeti-space-md — The default padding.
- --yeti-color-surface — The fill at data-surface="base"; the page's own surface.
- --yeti-color-surface-raised — The fill at data-surface="raised", a step above the page.
- --yeti-color-surface-sunken — The fill at data-surface="sunken", a step below the page.
Accessibility contract:
Purely visual. A box has no semantics; use a section or article element when the content is one.
## breakout (layout, class="breakout")
Keeps its children in a centered reading column with gutters, and lets any child carrying data-bleed span the full width.
Attributes:
- data-max: 3xs | 2xs | xs | sm | md (default) | lg | xl | 2xl — The widest the reading column may be.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — The gutter on each side, and the space between children.
On children:
- data-bleed on > * (boolean) — Breaks the child out of the column to span the full width.
- data-note on > * (boolean) — A margin note: in the end gutter beside the child it follows when the breakout is wide, in the column when it is not.
### When to use it
Long-form pages: an article, a case study, documentation. The text sits in a column narrow enough to read, and the occasional picture, quote band, or code block breaks out to the full width without leaving the flow.
### How it works
The breakout is a grid of three columns: a gutter, the reading column, and a gutter. The reading column is `data-max` wide, or the container minus two gutters when that is less, and the gutters share whatever remains, so the column is centered. Every child lands in the middle track. A child carrying `data-bleed` spans all three. The rows are separated by the gap, and children's own margins are zeroed, as in every gap-based layout. Prose is also capped at `--yeti-measure`, so a column set wider than the measure is not filled by paragraphs.
Children are spaced by `data-gap`, with one exception taken from the flow: a heading sits `--yeti-heading-space-before` below what comes before it and `--yeti-heading-space-after` above what it opens, `xl` and `sm` by default, whatever the gap, so a long read in a breakout reads like one in `article`. Set the first to `var(--yeti-space-3xl)` on the breakout for chapters that open wider.
```html
Readable.
Edge to edge.
Readable again.
```
A child carrying `data-note` is a margin note: it follows the paragraph it belongs to in the source, and when the content box is at least `xl` wide it moves into the end gutter beside that paragraph, in the small muted text of a hint; narrower, it stays in the column as an aside. One note per paragraph; a second note after the same paragraph stacks below the first and pushes the next paragraph down a row.
```html
The main text carries the argument.
```
### Why this name
The column is ordinary; what is special is that a child can break out of it. Foundation 6 had `.grid-container.fluid` for the whole page and nothing for one element; people reached for negative margins.
Children:
- > *: at least 1. The content, in the column. Any child may carry data-bleed to span the full width.
- > [data-bleed]: at least 0. Children that break out of the column.
- > [data-note]: at least 0. A margin note: in the end gutter beside the child it follows when the breakout is wide, in the column when it is not.
Tokens:
- --yeti-width-md — The default column width, chosen under the base measure so paragraphs fill it.
- --yeti-space-md — The default gutter and row gap.
- --yeti-heading-space-before — The space above a heading, counted from what comes before it.
- --yeti-heading-space-after — The space between a heading and what it opens.
- --yeti-text-sm — The note's text size.
- --yeti-color-text-muted — The note's text color.
Accessibility contract:
Purely visual. A bleeding child is still in reading order.
## center (layout, class="center")
Centers a column of content horizontally, up to a maximum width, with gutters on narrow screens.
Attributes:
- data-max: 3xs | 2xs | xs | sm | md | lg | xl (default) | 2xl — The widest the content column may be.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — The gutter on each side when the container is narrower than the maximum.
- data-intrinsic (boolean) — Shrink the column to its content (still no wider than data-max) and center it, with each child centered on its own width inside it.
### When to use it
A center is the page column: the wrapper that keeps a page or a section from spreading across a wide screen, and keeps it off the edges of a narrow one. Use `data-max="lg"` for reading, `xl` or `2xl` for pages with sidebars and grids.
### How it works
`max-inline-size` caps the content, automatic inline margins split the remaining space equally, and `padding-inline` supplies the gutters. The width is set explicitly, the container's less the gutters, rather than left to `auto`: inside a `stack`, which is a flex column, auto margins take the free space before alignment can stretch the item, and a center left to `auto` would shrink to its content. A page that is a stack of centered sections is the usual page, so it does not. The box is `content-box`, so `data-max` is the width of the content, not the content plus gutters. `data-intrinsic` changes what is centered: the column shrinks to fit its content, still no wider than `data-max`, and that box is centered on the page, with each child centered on its own width inside it. Use it for a heading and a button that should sit in the middle rather than stretch.
### Why this name
Centering is the whole job. Foundation 6 readers: this is `.grid-container`, with the width and gutters as attributes instead of Sass variables.
Tokens:
- --yeti-width-xl — The default maximum.
- --yeti-space-md — The default gutter.
Accessibility contract:
Purely visual.
## cluster (layout, class="cluster")
Lays its children out in a row that wraps, keeping one gap between them on both axes.
Attributes:
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between items, horizontally and between wrapped rows.
- data-align: start | center (default) | end | stretch | baseline — Vertical alignment of items within a row.
- data-justify: start (default) | center | end | between | around | evenly — How items are distributed along the row.
- data-threshold: 3xs | 2xs | xs | sm | md | lg | xl | 2xl — The cluster's own width below which it is a column of full-width items; at or above it, the wrapping row. Makes the cluster a size container, so it takes the width of its block instead of its content.
### When to use it
Use a cluster for a row of things that are sized by their content and may need to wrap: tags, buttons, navigation links, a logo beside a menu. When it wraps, the gap holds between rows as well as between items, so nothing needs a margin.
### How it works
A cluster is a wrapping flex row with a `gap`. `data-justify` distributes the items along the row, so `between` pushes the first and last to the edges, and `data-align` lines them up vertically within a row. Items keep their own width; a cluster never stretches them.
```html
css
layout
intrinsic
```
A term and its value on one line, parted to the two ends, is a cluster with `data-justify="between"`. A `div` may wrap a `dt` and its `dd`, so a whole legend is a `dl` that is a `stack` whose rows are clusters; add `data-rule` to the stack for a line between them and `data-numeric` to the values so the digits line up.
```html
Distance
14.2 km
Ascent
1,120 m
Time
5 h 30
```
### A column when it is narrow
`data-threshold` takes a width, `3xs` to `2xl`, and turns the cluster into a column of full-width items whenever its own width is below that size; at or above it, the cluster is the wrapping row again. It is for a row of actions or filters that sits in a sidebar or a stacked region, where a ragged wrap reads worse than a clean column.
```html
```
The cluster measures itself to decide, which makes it a size container, and a size container cannot take its width from its content. A thresholded cluster fills the block it is in, so give it one that has a width; a cluster without `data-threshold` is not a container and sizes to its items as before.
The trap is putting one where its width comes from its content. As an item of another cluster, or of any flex row that sizes its items by their content, a thresholded cluster has no content width to offer and collapses to nothing. Give it a width of its own there, or `flex-grow` so it takes the row's spare room; `npm run validate` warns when one sits directly in another cluster.
### Why this name
The word is exact: items gather, they do not line up in columns. Foundation 6 reached for `.button-group` or a menu for the same job.
Children:
- > *: at least 1. Anything sized by its content: tags, buttons, links, a logo and a nav.
Tokens:
- --yeti-space-md — The default gap.
Accessibility contract:
Purely visual. Use a list element when the items are a list.
## columns (layout, class="columns")
Lays its children out as equal columns when the container is wider than a threshold, and as rows when it is not.
Attributes:
- data-threshold: 3xs | 2xs | xs | sm | md (default) | lg | xl | 2xl — The container width below which the columns become rows.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between columns, and between rows once they stack.
- data-align: start | center | end | stretch (default) | baseline — Vertical alignment of the columns.
- data-justify: start (default) | center | end | between | around | evenly — Distribution of the columns along the row when they do not fill it.
- data-columns: 1 | 2 | 3 | 4 | 5 | 6 — The most columns on one row; further children each take a full row.
On children:
- data-span on > *: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 — How many shares of the row the child takes: 2 is twice a plain sibling. Below the threshold it stacks like the rest.
- data-shrink on > * (boolean) — The child is only as wide as its content and its siblings share the rest: an icon or a button beside text. Below the threshold it stacks like the rest.
### When to use it
Use columns for a set of equals: pricing tiers, feature summaries, a row of statistics. They should be side by side when there is room and one under another when there is not, and the container, not the screen, should decide which.
### How it works
Every child has the same `flex-basis`: the threshold minus the container's width, multiplied by a large number. In a container wider than the threshold that number is hugely negative, which `flex-basis` clamps to zero, so `flex-grow: 1` shares the row equally between them. In a narrower one it is hugely positive, so each child fills a row. `data-columns` caps how many share a row; any child past the cap takes a full row of its own.
```html
One
Two
Three, on its own row
```
A child with `data-span` takes that many shares of the row: `data-span="2"` beside a plain sibling is Foundation 6's eight-and-four, with no breakpoint, and it still stacks with the rest below the threshold.
```html
Two thirds.
```
A child with `data-shrink` is only as wide as its content and its siblings share the rest: Foundation 6's `shrink` cell beside an `auto` one, except that nothing need say `auto`, because a plain child already fills what is left. Use it for an icon, a date or a button beside text. Below the threshold it stacks with the rest.
```html
What the button adds to.
```
### Why this name
Yeti names layouts for what they do to their children, and what this one gives them is columns; the switching is the mechanism, not the point. Foundation 6 readers will recognise the job of `.grid-x` with `medium-4` cells; the difference is that the threshold is a container width, so the same markup works in a sidebar and a full-width band.
Children:
- > *: at least 2. The columns. Each gets an equal share of the row.
- > [data-span]: at least 0. A child that takes several shares of the row: data-span="2" is twice a plain sibling. Below the threshold it stacks like the rest.
- > [data-shrink]: at least 0. A child that is only as wide as its content, an icon or a button beside text; its siblings share the rest. Below the threshold it stacks like the rest.
Tokens:
- --yeti-width-md — The default threshold.
- --yeti-space-md — The default gap.
Accessibility contract:
Purely visual. Reading order is source order in both arrangements.
## container (layout, class="container")
Makes its box the thing a container query measures, so what is inside can respond to its width instead of the viewport's.
On children:
- data-show on *: 3xs | 2xs | xs | sm | md | lg | xl | 2xl — Show this element only while the nearest size container is this wide or wider. Below that width it is not rendered at all, and it is not in the accessibility tree either.
- data-hide on *: 3xs | 2xs | xs | sm | md | lg | xl | 2xl — Hide this element once the nearest size container is this wide or wider. Below that width it is rendered as usual.
### When to use it
Around anything that should change shape at its own width, when the thing itself cannot measure its own box. A layout can query itself and change its children, but nothing can change its own tracks from inside its own query; wrap it in a container and query that.
### How it works
One declaration: the box becomes a size container. Nothing else. To target it by name in your own CSS, give the container a name in your stylesheet, since a name cannot come from an attribute:
```css
.container.sidebar-slot { container-name: slot; }
@container slot (inline-size < 30rem) { .cluster { flex-direction: column; } }
```
The Yeti layouts that query themselves (`grid` with `data-fold` or `data-tracks`, `cluster` with `data-threshold`, `breakout` with notes, `timeline`) do not need one; this is for your own queries and for components that must restyle themselves.
Two markers read this box directly. `data-show` shows an element only while the container is at least that wide, and `data-hide` removes it once the container is that wide; both take a width from the same scale as `data-threshold`, and both mean "at or above", so `data-show="md"` is *shown from `md` up* and `data-hide="md"` is the same sentence the other way round. The widths are the tokens' defaults written as literals, because a container condition cannot read a custom property, so a theme that changes `--yeti-width-md` moves `data-threshold="md"` but not `data-show="md"`.
```html
Shown once this column is 32rem or wider.
Gone once this column is 32rem or wider.
```
They measure the nearest size container, not the window, which is the whole reason they exist: the same markup in a sidebar and in a full-width section makes two different decisions on one page. A `container` carrying `data-show` or `data-hide` itself is measured against its own nearest ancestor container, never its own box, because an element cannot query itself. Reach for them last. A component that can change shape at its own threshold should do that instead — the two versions of a thing, one shown and one hidden, are two things to keep in step forever — and an element removed this way is removed from the accessibility tree with it, so nothing a reader needs may live only in the narrow version.
With no size container anywhere above it, a container query never matches and neither marker does anything: the element stays visible at every width. That is the failure mode to know. These layouts are size containers with no conditions attached, so anything inside one is already covered: `container`, `nav`, `pagination`, `timeline`, and the preview box inside a `demo`. Five more are containers only in a particular shape — a `grid` with `data-fold` or with `data-tracks`, a `cluster` with `data-threshold`, a `card` whose first child is a figure, and a `breakout` holding a note — and anything else needs a `container` around it.
The [visibility guide](guides/visibility.md) puts these beside the platform's own ways of hiding something, and says which to reach for first.
### Why this name
It contains, and it is what a container query measures. Foundation 6's `.grid-container` was a page column; that job is `center`.
Children:
- > *: at least 1. Anything. The container itself arranges nothing.
Accessibility contract:
Purely structural.
## cover (layout, class="cover")
Fills at least the viewport's height and centers one child vertically, with optional content pinned above and below it.
Attributes:
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Minimum space between the centered child and whatever sits above or below it.
- data-height: sm | md | lg | xl | half | full (default) — The band's least height: a stop of the height scale, half the viewport, or the whole of it. Content taller than the band grows it; nothing is cut off.
On children:
- data-center on > * (boolean) — Centers the child in the space the others leave; exactly one child carries it.
### When to use it
A cover is the hero section, the sign-in screen, the "coming soon" page: one thing centered in the height of the viewport, with a navigation bar above it or a footnote below it when you want them.
### How it works
The cover is a flex column with a minimum block size: the viewport by default, from `--yeti-cover-height`, `100dvh` so it tracks the browser chrome on phones, or whatever `data-height` names, a stop of the height scale from `sm` to `xl`, `half` the viewport, or `full`. It is a minimum and never a fixed size, so a band whose content is taller than its name simply grows; nothing is cut off. A band with a heading in the middle of it is the answer to "more padding than the scale has": `data-height="md"` with the heading marked `data-center`. The child marked `data-center` gets automatic block margins, which take up all the free space equally above and below it; anything else sits at its natural size at the top or bottom. The gap keeps the centered child from touching its neighbours when the content is taller than the viewport.
### Why this name
The block covers the viewport. Foundation 6 had no primitive for this; people combined a full-height utility with vertical alignment classes on the grid.
Children:
- > *: at least 1. A header, the centered child, a footer: any of them optional except the centered one.
- > [data-center]: at least 1, at most 1. Exactly one child carries data-center and is centered in the remaining space.
Tokens:
- --yeti-cover-height — The least height with no data-height, and at data-height="full".
- --yeti-space-md — The default gap.
Accessibility contract:
Purely visual. Keep the page's h1 inside the cover when the cover is the hero.
## frame (layout, class="frame")
Holds one child in a fixed aspect ratio, cropping an image, video or embedded document to fill it and centering anything else.
Attributes:
- data-ratio: 1/1 | 4/3 | 3/2 | 16/9 (default) | 21/9 — Width to height.
### When to use it
Use a frame wherever images of unpredictable shape must present as the same shape: card thumbnails, avatars, a video embed that should not reflow the page while it loads. The frame owns the shape; the media fills it.
### How it works
`aspect-ratio` sizes the frame from its width, and `overflow: hidden` clips. An image, video, `picture`, `iframe`, `embed` or `object` child is stretched to both dimensions with `object-fit: cover`, so it fills without distortion and loses only the overflow. Any other child, a placeholder or an icon, is centered with flexbox.
A frame that wants an edge, a map's neatline, a photo on a light page, takes `data-border`, which draws the border width in the border color around any element. For a different color set `--yeti-color-border` on the frame itself.
```html
```
A toned plate is a `filter` on the frame's media, the page's own art direction:
```html
```
### Why this name
It is what a picture frame does: fix the shape and crop what is inside it. Foundation 6 had `.responsive-embed` for video ratios only; a frame does the same for anything.
Children:
- > *: at least 1, at most 1. One child: an image, video, picture, iframe, embed or object is stretched to fill and cropped; anything else is centered.
Accessibility contract:
Cropping hides parts of an image. Make sure the alt text describes what is visible, or use data-ratio to match the image so nothing is lost. An iframe needs a title saying what is in it, whatever shape it is cropped to.
## grid (layout, class="grid")
Fits as many equal columns as the container allows at a minimum width, up to an optional maximum count.
Attributes:
- data-min: none | 3xs | 2xs | xs (default) | sm | md | lg | xl | 2xl — The narrowest a column may be. With none, only data-columns decides the count; none is meant to be paired with data-columns, and alone gives a single full-width column.
- data-columns: 1 | 2 | 3 | 4 | 5 | 6 — The most columns allowed. Fewer appear when the container cannot fit that many at data-min; with data-min="none" the count is exact. With data-fold, the count only ever halves.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between columns and rows.
- data-align: start | center | end | stretch (default) | baseline — Vertical alignment of the cells within their row: stretch fills the row, end sits a short cell on the row's bottom edge.
- data-rows: 2 | 3 | 4 | 5 | 6 — Line up the parts of the children across each row: the value is how many rows each child spans, one per part.
- data-fold (boolean) — Halve the column count as the grid narrows instead of stepping down one at a time. Needs data-columns 2, 4, or 6 and uses data-min as the width per column. data-min="none" has no meaning with a fold: the count needs a width per column.
- data-tracks: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 — A fixed number of equal tracks that children are placed on with data-start and data-span, leaving any track nothing is placed on empty. Replaces the fitted columns, so data-min and data-columns have no effect alongside it, and it does not mix with data-fold.
- data-threshold: 3xs | 2xs | xs | sm | md (default) | lg | xl | 2xl — With data-tracks: the grid's own width below which every child takes the whole row, in source order; at or above it, the placement applies.
On children:
- data-start on > *: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 — In a tracks grid, the column line the child begins on. Without it the child flows into the next free track.
- data-span on > *: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 — In a tracks grid, how many tracks the child covers. Without it the child covers one.
### When to use it
Use a grid for a collection of like things that should line up in equal cells: cards, thumbnails, team members, products. The grid decides how many columns fit; you decide how narrow a cell may get and, when it matters, how many columns there may be at most.
### How it works
The track list is `repeat(auto-fit, minmax(, 1fr))`, where `` is the larger of `data-min` and the container divided by `data-columns`. Without `data-columns` that second value is zero, so the minimum width alone decides the count. With it, the container split N ways becomes the floor as soon as it is wider than the minimum, so there are never more than N columns, and still fewer when even N would squeeze a cell below the minimum. `data-min="none"` removes the width floor and gives exactly N. `data-min="none"` is meant to be paired with `data-columns`; alone it gives a single full-width column instead of a runaway number of tracks.
```html
Always
four
across
here
```
Add `data-fold` and the count halves instead of stepping: with `data-min="xs"` and `data-columns="4"` the grid is four across while its content box is at least four `xs` widths, two by two while it is at least two, and a single column below that, never three. The thresholds are the width token's default multiplied by the count, so a theme that changes the token moves the token, not the fold. The closest thing with no container query is two `columns` nested in a third, and it is not the same result: it steps at its own thresholds (inner `sm`, outer `md`) rather than the fold's.
```html
One
Two
Three
Four
```
The fold is one attribute on one element; the nest is two wrappers. Use whichever you would rather explain.
`data-rows` lines up neighbours' parts: with `data-rows="3"` each child is a subgrid of three rows, so every first part sits in row one, every second in row two, and so on, across the row. Give the number of parts the fullest child has; a child with fewer leaves its last rows empty. A card in a grid with rows keeps its picture and footer aligned with its neighbours' and does not switch to its side-by-side row. A child that spans rows cannot also be a size container, because a size container cannot be a subgrid; the card turns its own container off inside such a grid for this reason.
`data-min="3xs"`, 8rem, is the stop for things that want two abreast on a phone: a column of links, a list of badges, a gallery of small pictures. The default `xs` gives a phone one column of them.
`data-align` places the cells on the cross axis. Cells stretch to the height of their row by default, which is what a row of cards wants; `data-align="end"` sits a short cell on the row's bottom edge, so a column of links or badges under a taller neighbour lines up along the bottom, and `center` and `start` do what they say. The words are the same as on `columns` and `cluster`.
### Tracks
`data-tracks` is the other mode: instead of fitting as many columns as there is room for, the grid has exactly that many equal tracks, two to twelve, and each child is placed on them by line. `data-start` on a child is the column line it begins on and `data-span` how many tracks it covers, both one to twelve. A child with neither flows into the next free track after the child before it and covers one; with only a span it flows the same way and covers that many; with only a start it begins there and covers one. A track nothing is placed on stays empty, which is the point: a wall with a work from line 2 across six tracks and a second from line 9 across four leaves track 1 and track 8 bare.
```html
The first workThe second work
```
A tracks grid measures itself, so it is a size container. Below `data-threshold` (`md` when absent), every child takes the whole row, in source order, so the wall reads as a column on a phone; at or above it, the placement applies. `data-tracks` replaces the fitted columns, so `data-min` and `data-columns` do nothing alongside it. It does not mix with `data-fold` either: the fold does nothing on a tracks grid, and `npm run validate` refuses the pair. A grid without `data-tracks` is not a container and none of this applies to it.
### Why this name
It is a grid and nothing else is. Foundation 6 readers: this replaces the Block Grid, and `data-columns="4"` is the intrinsic form of `large-up-4`, with the shrinking at narrow widths handled by the minimum instead of by `small-up-1 medium-up-2`.
Children:
- > *: at least 1. The cells. All are the same width, except in a tracks grid, where each covers the tracks it is placed on.
Tokens:
- --yeti-width-xs — The default minimum column width.
- --yeti-space-md — The default gap.
Accessibility contract:
Purely visual. Use a list when the cells are a list of like things.
## icon (layout, class="icon")
Sizes an inline SVG to the surrounding text and aligns it with the text beside it.
Attributes:
- data-gap: none | xs (default) | sm | md | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between the icon and its text.
- data-align: start | center (default) | end | stretch | baseline — How the icon lines up with the text: centered on the line, or on the baseline.
### When to use it
Wrap an inline SVG in an icon whenever it should behave like a character: in a link, a button, a list item, a heading. It takes the size and color of the text around it and stays aligned with it, at any font size.
### How it works
The wrapper is an inline flex row with a small gap. The SVG is sized to one em, so it follows the text size, and `currentColor` inside it follows the text color. `data-align` chooses between centering on the line, which suits buttons and labels, and sitting on the baseline, which suits running text; the baseline setting nudges the glyph down an eighth of an em, because an icon whose bottom edge sits exactly on the baseline reads as floating.
```html
```
### Why this name
An icon is what it holds. Foundation 6 shipped an icon font instead, which set size and alignment by font rules; inline SVG needs this small layout to do the same.
Children:
- > svg: at least 1, at most 1. The icon, an inline SVG, sized to one em.
Tokens:
- --yeti-space-xs — The default gap.
Accessibility contract:
When the icon sits beside text, give the SVG aria-hidden="true" so the text is the name. When it stands alone, give the SVG role="img" and an aria-label.
## layer (layout, class="layer")
Stacks its children in one box, later ones on top, with the box as tall as the tallest of them.
Attributes:
- data-align: start | center | end | stretch (default) | baseline — Vertical alignment of every child in the box. baseline behaves as start here.
On children:
- data-align-self on > *: start | center | end | stretch | baseline — Where the child sits vertically: start, center, end, stretch, or baseline.
- data-justify-self on > *: start | center | end | stretch — Where the child sits horizontally: start, center, end, or stretch.
### When to use it
A caption over a picture, a badge in the corner of a thumbnail, a heading over a hero image, a loading message over a form. Anything that sits on top of something else and should still take part in the layout: the box is as tall as its tallest child, so nothing overlaps its neighbours.
### How it works
Every child is placed in the same grid cell, in source order, so later children paint over earlier ones. `data-align` on the layer sets where every child sits vertically when it is shorter than the box; by default they stretch to fill it. To place one child on its own, put `data-align-self` and `data-justify-self` on that child: a badge in the top-right corner is `data-align-self="start" data-justify-self="end"`. Compare `overlay`, which takes one marked child out of the flow and centers it over the rest.
```html
New
```
### Why this name
Layers is what every design tool calls things stacked in one frame. Foundation 6 readers did this with a positioned wrapper and an absolutely positioned child, which is what `overlay` still does when you need it.
Children:
- > *: at least 2. The layers, bottom first.
- > [data-align-self]: at least 0. A child that places itself vertically: start, center, end, stretch, or baseline.
- > [data-justify-self]: at least 0. A child that places itself horizontally: start, center, end, or stretch.
Tokens:
- --yeti-color-scrim — Wash behind a figure's caption, so it reads over the picture.
- --yeti-color-text — A figure caption over the picture, which is not the aside a caption under one is.
- --yeti-space-xs — Block padding of that caption.
- --yeti-space-sm — Inline padding of that caption.
Accessibility contract:
Everything stays in reading order. Text laid over a picture needs a contrast treatment (a scrim or a solid panel); the layer does not add one.
## masonry (layout, class="masonry")
Packs items of uneven height into columns with no gaps under the short ones.
Attributes:
- data-min: none | 3xs | 2xs | xs (default) | sm | md | lg | xl | 2xl — The narrowest a column may be. none is meant to be paired with data-columns, which then gives an exact count; alone it gives a single full-width column.
- data-columns: 1 | 2 | 3 | 4 | 5 | 6 — The most columns allowed. Fewer appear when the container cannot fit that many at data-min.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between columns and between items.
### When to use it
A wall of photos, cards of unpredictable length, quotes of different sizes: anything where equal rows would leave holes under the short items. If the items are roughly equal, use a `grid` and keep the rows.
### How it works
Everywhere, the masonry is a multi-column layout: `data-min` is the column width, `data-columns` caps the count, and each item is told not to break across columns. In that form items flow down the first column, then the next, so the reading order runs in columns. Where the browser supports native masonry, the same element becomes a grid with masonry rows, items read across as in a grid, and the gap is a real gap. Both forms use the same attributes and the same tokens, so nothing in your markup changes when a browser gains support. In the multi-column fallback the last item of each column keeps its bottom gap, so the fallback is one gap taller than the native form.
### Why this name
The word everyone already uses for the brick-wall arrangement. Foundation 6 had nothing for it; people reached for a JavaScript library, which is the thing this layout replaces.
Children:
- > *: at least 2. The items. Each keeps its own height.
Tokens:
- --yeti-width-xs — The default minimum column width.
- --yeti-space-md — The default gap.
Accessibility contract:
In browsers without native masonry, items read down each column and then across. Keep the source order meaningful on its own.
## overlay (layout, class="overlay")
Holds one child over the rest, centered, without pushing anything around; the parent is the box it covers.
Attributes:
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — The least space kept between the held child and the edges of the box.
- data-fixed (boolean) — Center the held child over the viewport instead of this box, and keep it there while the page scrolls.
On children:
- data-over on > * (boolean) — Lifts the child out of the flow and centers it over the box; exactly one child carries it.
- data-fill on > [data-over] (boolean) — On the same child: cover the box rather than sit centred in it, which is what a veil, a dimmer or a loading state wants. Positioning only, so a veil that wants its own content centred puts a layout inside itself.
### When to use it
An overlay holds one thing on top of another without taking part in the layout underneath: a "sold out" stamp on a product image, a loading message over a form, a notice over the whole page with `data-fixed`. It is the positioning half of a dialog; the behaviour half is a component's job. Compare `layer`, which stacks all its children in flow and grows to the tallest; an overlay's held child changes nothing about the box.
### How it works
The overlay is the box being covered, so it is positioned, and its plain children flow as usual. The one child carrying `data-over` is absolutely positioned with its top-left corner at the box's center, then translated back by half its own size, which centers it whatever its dimensions. Its maximum width and height are the box's minus a gap on each side, and it scrolls internally rather than growing beyond that. `data-fixed` on the overlay makes the held child `fixed`, so the box it centers on is the viewport.
`data-fill`, on the same child as `data-over`, covers the box instead of sitting centred in it. The gap is the inset a centred child keeps from the edges and a cover has none, so the size caps go with it. That is what a veil, a dimmer or a loading state wants, and it only pays off with something translucent behind it: `--yeti-color-scrim` is the framework's wash for exactly this, the surface thinned so it dims with the theme rather than tinting. On a `stack`, `data-fill` is the layout's own height instead: at least the viewport's.
It positions and nothing else. A veil that wants its own content centred puts a layout inside itself, the way every other layout composes:
```html
```
```html
```
### Why this name
Overlay is what everyone already calls a thing that lies over other things. Foundation 6's Reveal was the modal; the plain positioning had no name.
Children:
- > *: at least 2. The content underneath, in normal flow, and one held child.
- > [data-over]: at least 1, at most 1. Exactly one child carries data-over; it leaves the flow and is centered over the box.
Tokens:
- --yeti-space-md — The default edge gap.
Accessibility contract:
Positioning only. The held child stays in reading order where it is in the source. A held child that interrupts the page needs focus management and a way to dismiss it, which the dialog and tooltip components provide; use those rather than this primitive alone for anything modal.
## scroller (layout, class="scroller")
Lays its children out in a single row that scrolls horizontally.
Attributes:
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between items.
- data-width: 3xs | 2xs | xs | sm | md | lg | xl | 2xl — Give every item this width instead of its natural one.
- data-snap (boolean) — Snap each item's start edge into place as the track scrolls.
- data-justify: start | center | end — Where each item settles along the track when data-snap is set: its start edge, its center, or its end. Nothing without data-snap.
### When to use it
A scroller is for a row that should stay a row: a strip of photos, a set of related cards, a filmstrip of steps. Where a cluster would wrap, a scroller keeps everything on one line and lets the visitor move along it.
### How it works
A flex row that does not wrap, with `overflow-x: auto`. Children are told not to shrink, so the row overflows and the container scrolls. `data-snap` adds scroll snapping so items land cleanly, at their start edge unless `data-justify` says `center` or `end`; `data-width` gives every item the same width so the strip reads as a sequence of equal frames. Because a scrolling region is an interactive one, the markup carries `tabindex="0"` and an accessible name; the validator insists. A media child (an `img`, a `picture`, a `video`, a `canvas`, an `svg`) is also exempt from the base reset's media cap, so one wider than the track overflows it instead of shrinking to fit; `data-width` still sets a child's width when one is wanted. A text child (a paragraph, a list item) is not media, so it keeps the usual prose measure instead of running to one long line. The scroller is a positioned ancestor, so a child positioned absolutely (a `visually-hidden` label in a header cell, say) stays inside the track and scrolls with it, instead of escaping to position against the page.
### Why this name
What the visitor does is scroll, so the layout is a scroller; a name for the mechanism (a reel, a track) would say less. Foundation 6 had no layout for a row that scrolls; people hid the overflow by hand.
Children:
- > *: at least 1. The items. None shrinks; the track scrolls instead. A single wide item (a table, an image) is legal: the scroller then just gives that one item horizontal overflow.
Tokens:
- --yeti-space-md — The default gap.
Accessibility contract:
Required: tabindex, aria-label | aria-labelledby
- Tab: Focuses the track so the arrow keys can scroll it.
- ArrowLeft / ArrowRight: Scroll the track.
A scrollable region must be reachable from the keyboard: give it tabindex="0", role="region", and a name with aria-label.
## sidebar (layout, class="sidebar")
Places a fixed-width sidebar beside flexible content, and stacks them when the content would drop below half the width.
Attributes:
- data-side: start (default) | end — Which child is the sidebar: the first (start) or the last (end).
- data-width: 3xs | 2xs | xs | sm (default) | md | lg | xl | 2xl — The sidebar's preferred width.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between the two children, side by side or stacked.
- data-align: start | center | end | stretch (default) | baseline — Vertical alignment of the two children when side by side.
On children:
- data-sticky on > * (boolean) — Pins this child at --yeti-sticky-offset from the top of the scrollport while the other one scrolls past it. It takes its own height rather than the row's, since a stretched item has nowhere to move.
### When to use it
A sidebar pairs something with a natural width, such as a navigation list, a figure, or a form's summary, with content that should take whatever is left. It handles the awkward middle widths on its own: the two sit side by side as long as the content keeps at least half the space, and stack when it cannot.
### How it works
Both children live in a wrapping flex row. The sidebar child gets `data-width` as its basis; the content child grows aggressively from a zero basis and refuses to shrink below half the container. When the container is too narrow for sidebar plus half, the content wraps to its own line and both fill the width. There is no breakpoint; the switch happens wherever the numbers say it should.
```html
Main content first in the source.
```
Either child can carry `data-sticky` and stay put while the other scrolls past it, which is what a section nav beside a long article wants. It stops `--yeti-sticky-offset` from the top of the scrollport and takes its own height rather than the row's: a flex item stretched to the height of the row it is in has nowhere left to move, and would never stick at all.
### Why this name
The word says exactly what the fixed child is. Foundation 6 built this with `.grid-x` columns and `medium-8`/`medium-4` classes, which fixed the switch to a viewport breakpoint rather than to the content.
Children:
- > *: at least 2, at most 2. Exactly two: the sidebar and the content, in either order according to data-side.
Tokens:
- --yeti-width-sm — The default sidebar width.
- --yeti-space-md — The default gap.
Accessibility contract:
Purely visual. Put the content first in the source when it matters more, and use data-side="end" to show the sidebar after it.
## stack (layout, class="stack")
Stacks its children vertically with one consistent gap between them.
Attributes:
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between children. A pair such as sm-lg grows fluidly from the first stop to the second.
- data-align: start | center | end | stretch (default) | baseline — Horizontal alignment of the children.
- data-fill (boolean) — Make the stack at least as tall as the viewport, so a child carrying data-split reaches the bottom.
- data-rule (boolean) — A line between children, of the border width in the border color, in the middle of each gap. The gap is unchanged.
On children:
- data-split on > * (boolean) — Pushes the child and everything after it to the end of the stack when the stack is taller than its content.
- data-space on > *: none | xs | sm | md | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — The gap before this one child, larger or smaller than the stack's own: more room above a heading, less between a label and its field. Does nothing on the first child; data-split wins on a child carrying both.
- data-sticky on > * (boolean) — Pins this child at --yeti-sticky-offset from the top of the scrollport while the rest of the stack scrolls past it. It keeps the full width of the column, a stack stretching its children on the inline axis.
### When to use it
Reach for a stack whenever things sit one above another and should be spaced evenly: the sections of a card, the fields of a form, the parts of a sidebar. Nest stacks with different gaps to express hierarchy: a large gap between sections, a small one inside each.
### How it works
The stack is a flex column with a `gap`, so the space between children is the stack's decision and children's own margins are zeroed. `data-align` controls horizontal alignment, and the default is to stretch, so an inline thing put straight into a stack, a badge or a button, becomes as wide as the column; give the stack `data-align="start"` or put the thing in a paragraph. Give one child `data-split` and it, with everything after it, moves to the end whenever the stack is taller than its content, which is how a card keeps its actions at the bottom. Add `data-fill` and the stack is at least as tall as the viewport, which with a `data-split` footer is the whole of a sticky footer. It is the stack that fills; on a child of an `overlay`, the same word means the child covers its box.
```html
```
The gap is the stack's, but one child can ask for a different gap before it. `data-space` on a child takes the same words as `data-gap` and sets the space above that child alone, larger or smaller than the stack's own: more room above a heading that opens a new run of paragraphs, less between a label and the field under it. The rest of the stack keeps its rhythm. It does nothing on the first child, which has no gap before it, and `data-split` wins on a child carrying both. This is Yeti's margin utility, and it lives on the layout on purpose: the exception is one word on one child, and the stack still owns the rule.
```html
One paragraph.
Another, at the stack's gap.
A heading with room above it
And its paragraph, back at the stack's gap.
```
`data-rule` on the stack draws a line between each pair of children, the border width in the border color, in the middle of the gap; the gap itself does not change, and `data-space` moves the line with it. A legend, a settings list, a run of plan features: anywhere rows want separating without boxing. The line is a `::before` on each child after the first, so a child that already draws its own `::before` keeps its own and loses the line. The line is lost too on a child that clips its overflow, a card, a frame, an accordion or a progress bar, and on a replaced element such as an image, which has no `::before` to draw. A child that is itself a stack still has its line placed by the ruled stack's gap, but a ruled stack directly inside another ruled stack places its line by its own gap, the same limit `data-space` has; give it a plain wrapper.
```html
Distance
14.2 km
Ascent
1,120 m
```
A child carrying `data-sticky` stays at `--yeti-sticky-offset` from the top of the scrollport while the rest of the stack scrolls past it, and keeps the full width of the column while it does. A stack stretches its children sideways, and sideways is not the direction a sticky child moves in, so nothing has to be taken away for it to work — which is not true in a row, where the same marker costs the child the row's height.
### Why this name
No other word says it as plainly: things stacked, one on another. Foundation 6 had no equivalent; the space between blocks came from each element's own margins.
Children:
- > *: at least 1. Anything. Each child's margins are reset; the stack owns the space between them.
- > [data-split]: at least 0, at most 1. At most one child may carry data-split; it and everything after it are pushed to the end of the stack when the stack is taller than its content.
Tokens:
- --yeti-space-md — The default gap.
- --yeti-cover-height — The minimum block size when data-fill is set.
- --yeti-border-width — Width of the data-rule line.
- --yeti-color-border — Color of the data-rule line.
Accessibility contract:
Purely visual. Reading order is source order.
## timeline (layout, class="timeline")
Lays its entries along a rail with a marker each, on one side, or on alternate sides of a centred rail when it is wide.
Attributes:
- data-gap: none | xs | sm | md | lg (default) | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between entries.
- data-alternate (boolean) — Put entries on alternate sides of a centred rail once the list is wide enough.
### When to use it
A history, a changelog, the steps of a process, a schedule: entries in order along a line. It is an ordered list with a rail, so it reads as a list and looks like a timeline.
### How it works
The list is a column of entries at the gap, with a rail down its start edge and a marker on the rail beside each entry. Add `data-alternate` and, once the list's content box is at least `lg` wide, the rail moves to the centre and the entries take turns on either side of it, markers still on the rail; narrower, they fall back to one side. The switch is the list's own width, so a timeline in a sidebar stays single-sided while the same one across a page alternates.
```html
Started.
Shipped.
```
### Why this name
It is a line of time. Foundation 6 had nothing for it; people reached for a snippet and a breakpoint.
Children:
- > li: at least 2. The entries, in order. Put a time, a heading, and text inside each.
Tokens:
- --yeti-space-lg — The default gap, and the room between the rail and the entries.
- --yeti-color-border-strong — The rail and the markers.
Accessibility contract:
Use an ordered list, so the sequence is announced, and a time element with datetime in each entry. The rail and markers are decorative. Give the ol role="list": removing the list markers removes the list semantics in some engines.
## hero (recipe, class="hero")
Fills the viewport's height with copy on one side and a figure on the other, centered, and stacks them below a container width; the one-class form of a cover holding columns.
Attributes:
- data-threshold: 3xs | 2xs | xs | sm | md | lg (default) | xl | 2xl — The container width below which copy and figure become rows.
- data-gap: none | xs | sm | md | lg (default) | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between copy and figure.
- data-ratio: 1/1 | 4/3 (default) | 3/2 | 16/9 | 21/9 — The figure's aspect ratio.
- data-align: start | center (default) | end | stretch | baseline — Vertical alignment of copy and figure when side by side.
- data-side: start | end — Put the figure at the start or the end of the row regardless of source order. Once stacked, source order rules: write the copy first to meet the headline first on a phone. Moves it visually only; reading order stays as written.
- data-height: sm | md | lg | xl | half | full (default) — The band's least height: a stop of the height scale, half the viewport, or the whole of it, as a cover takes it. Content taller than the band grows it.
On children:
- data-min on > *: none | 3xs | 2xs | xs | sm | md | lg | xl | 2xl — The narrowest this child may get while the two share a row; the other gives way. Stacked, it has no effect.
- data-span on > *: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 — How many shares of the row this child takes; the other child's default is 1.
### When to use it
The opening of a landing page: a headline and a call to action on one side, a picture on the other, filling the first screen, or a shorter band with `data-height`, as a cover takes it. Below the threshold the two become rows and the band grows to fit. Write the copy first, so a phone meets the headline before the picture, and use `data-side` to put the picture on the side you want while the two share a row; once stacked the source order holds. The copy must not have an img, video, or picture as a direct child (it would be taken for a second figure).
The two halves share the row equally unless a child says otherwise: `data-span="3"` on the picture beside `data-span="2"` on the copy gives the picture three fifths, the same marker `columns` uses. Below the threshold both are full width. Shares alone squeeze the copy at a middling width, so `data-min` on a child sets the narrowest it may get while side by side, from the width scale: `data-min="sm"` on the copy keeps its buttons in a row and the picture gives way instead. Stacked, it does nothing.
A headline that should grow with the band can carry the [billboard](billboard.md) utility. Make the copy a `container` first, so the line is sized by the column it is in and not by the whole band:
```html
Build interfaces that read their own container
Fifteen layouts, one attribute vocabulary, no breakpoints.
```
### Built from primitives
A `cover` centers one child in the viewport's height. Give it `columns` carrying `data-center`, and inside the columns a `stack` for the copy and a `frame` for the picture. The threshold on the columns is what turns the two halves into rows.
```html
Build interfaces that read their own container
Fifteen layouts, one attribute vocabulary, no breakpoints.
```
The one-class form does the same in one element: a wrapping row whose lines are centered in the band's height. Its test measures both forms against each other.
### Why this name
"Hero" is what designers have called the big opening image since the print era, and every framework since Bootstrap 2 has shipped one under that name. The split is the common form; a hero with no picture is a `cover`.
Children:
- > *: at least 2, at most 2. Exactly two: the copy and the figure (an img, video, picture, canvas or svg, or an element wrapping one); the copy (the body) must not have any of those as a direct child (it would be taken for a second figure). A figure with a figcaption keeps its caption below the picture.
- > [data-span]: at least 0, at most 2. A child that takes several shares of the row: data-span="3" beside a "2" is three fifths. Below the threshold both stack full width.
Tokens:
- --yeti-cover-height — The least height with no data-height, and at data-height="full".
- --yeti-width-lg — The default threshold.
- --yeti-space-lg — The default gap.
- --yeti-space-sm — The gap between the copy's children.
- --yeti-text-sm — Text size of the caption.
- --yeti-color-text-muted — The caption.
Accessibility contract:
When the hero is the page's opening, keep the h1 inside the copy and write the copy first: data-side changes where the figure sits in the row, not where it is read, and not the stacked order.
## media (recipe, class="media")
Puts a figure beside a block of text and stacks them when the text would drop below half the width; the one-class form of a sidebar holding a frame and a stack.
Attributes:
- data-width: 3xs | 2xs | xs (default) | sm | md | lg | xl | 2xl — The figure's preferred width.
- data-ratio: 1/1 (default) | 4/3 | 3/2 | 16/9 | 21/9 — The figure's aspect ratio.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between the figure and the text.
- data-align: start | center | end | stretch (default) | baseline — Vertical alignment of figure and text when side by side.
- data-max: 3xs | 2xs | xs | sm | md | lg | xl | 2xl — The widest the text may run side by side; the figure takes the rest of a wide row. data-width still sets where the two stack.
- data-side: start | end — Put the figure at the start or the end while side by side, regardless of source order. Stacked, the source order holds.
### When to use it
A comment with an avatar, a product with its thumbnail, a speaker with a headshot: whenever a picture sits beside a few lines of text, this is the shape. It stays side by side while the text keeps at least half the width and stacks when it cannot. Put the picture first for picture-left, last for picture-right; the reading order follows. The figure is whatever the reset counts as a picture: an `img`, `video`, `picture`, `canvas` or inline `svg`, bare or inside one wrapper, so a mark drawn in the page sits beside its text like a photograph. The body must not have any of those as a direct child (it would be taken for a second figure); an inline `svg` icon in the body goes beside its heading in a paragraph, or in a wrapper of its own.
`data-width` is the figure's width, and so where the two stack: a smaller figure holds the row down to a narrower window. On a wide row that leaves the text running long, so `data-max` caps the text from the width scale and the figure takes the rest. `` stays side by side down to a phone and still keeps a reading measure at full width. The text's half of the row wins over the cap.
### Built from primitives
A recipe is a shortcut, not a new idea. The same result is three layouts you already know: a `sidebar` whose first child is a `frame` and whose second is a `stack`. Reach for this form when you want to change any part of it, since every knob is an attribute you can see.
```html
Ada Lovelace
Wrote the first published algorithm, for Babbage's Analytical Engine.
```
The one-class form renders the same geometry, and its test proves it. Its attributes are the ones the primitives take. Its CSS is its own file, so a project that prefers the composed form can leave `css/recipes/media/media.css` out of a hand-built bundle. One difference: the sidebar's `data-side` picks which child is the sidebar, while the recipe's `data-side` reverses the row, which changes where the figure sits beside the text but not where it is read, and does nothing once the two have stacked.
### Why this name
Nicole Sullivan named the media object in 2010 and the name stuck across a decade of frameworks, Foundation 6's `.media-object` included. One word of it is enough.
Children:
- > *: at least 2, at most 2. Exactly two: the figure (an img, video, picture, canvas or svg, or an element wrapping one) and the body; the body must not have any of those as a direct child (it would be taken for a second figure), so an inline svg icon inside the body goes in a wrapper or beside its heading in a paragraph. A figure with a figcaption keeps its caption below the picture.
Tokens:
- --yeti-width-xs — The default figure width.
- --yeti-space-md — The default gap.
- --yeti-space-sm — The gap between the body's children.
- --yeti-text-sm — Text size of the caption.
- --yeti-color-text-muted — The caption.
Accessibility contract:
The figure is cropped to its ratio, so alt text should describe what is visible. data-side changes where the figure sits, not where it is read.
## shell (recipe, class="shell")
The page skeleton: header on top, footer at the bottom of the viewport when the page is short, and a body row where nav and aside sit beside main while there is room.
Attributes:
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between the rows, and between nav, main, and aside.
- data-width: 3xs | 2xs | xs | sm (default) | md | lg | xl | 2xl — The preferred width of nav and aside. Three regions share a row once the container is about four times this width plus the gaps; two regions need about half that.
On children:
- data-sticky on > div > :is(nav, aside) (boolean) — Pins the nav or the aside at --yeti-sticky-offset from the top of the scrollport while main scrolls past it.
### When to use it
Every page has a skeleton, and most of them are this one: a header, a footer that should sit at the bottom of the screen even when the page is short, and a middle that may have navigation down one side or related material down the other. Put the class on `body` and the landmarks do the rest. The parts are spaced with a margin on each part after the first rather than with a gap on the column, so an element a browser extension appends to `body` earns no space of its own and the footer still ends the page. Three regions share a row once the container is about four times `data-width` plus the gaps; two regions need about half that.
The body row's `nav` and `aside` can each carry `data-sticky` and stay in place while `main` scrolls past them. Each stops `--yeti-sticky-offset` from the top of the scrollport and keeps its own height instead of the row's, which is what lets it move at all.
### Built from primitives
A `stack` with `data-fill` is at least as tall as the viewport, and a footer carrying `data-split` drops to its bottom. In the middle, a `sidebar` puts the nav beside the main content until the content would fall below half the width. That is the whole page.
```html
Site header
Page title
The main content, which takes what is left.
```
The one-class form adds the third region: an `aside` after `main` in the same body row takes the same width as the nav and sits at the end. The body row needs its own element, the `div`, because three regions cannot share a row with a header and footer above and below them without one; a page with only `main` may skip it.
### Why this name
A shell is the part of the page that is there before any content is, and stays when the content changes. Foundation 6 readers built this with `.grid-container` and a sticky-footer snippet from the forums; the shell is both, with the landmarks as the API.
Children:
- > *: at least 1. In order: an optional header, the body (a div holding nav, main, and aside, in that source order, or a bare main when there are no side regions), and an optional footer.
- > header: at least 0, at most 1. undefined
- > footer: at least 0, at most 1. undefined
- > main: at least 0, at most 1. undefined
- > div: at least 0, at most 1. The body row. Its nav and aside take data-width; its main takes the rest and never drops below half.
Tokens:
- --yeti-cover-height — The shell's minimum block size; the viewport by default.
- --yeti-space-md — The default gap.
- --yeti-width-sm — The default nav and aside width.
Accessibility contract:
Meant for body or the page's outermost element. The landmarks do the work: one main; a label on nav when the page has more than one; aside for content that is complementary, not primary.
## accordion (component, class="accordion")
A column of disclosures built on details and summary, each opening to show its panel, with the browser doing the opening, the keyboard, and the announcing.
### When to use it
A run of questions and answers, a set of sections most readers only want one of, a long form split into stages. Not for content everyone needs: if it matters, leave it open.
### How it works
Each panel is a `details` with a `summary`, so the opening, the keyboard, and the announced state come from the browser rather than from Yeti. The base layer dresses a bare `details` already — a strong summary, a chevron that turns, a panel that grows open — so the component adds only what makes a column of them one object: the border around the set, the line between rows, the summary's own surface, and the padding.
Each summary sits on its own surface, clearly apart from the answer below it, and moves a step further under the pointer. That is what keeps a set of rows reading as rows rather than as one undivided block; an open one also keeps a line under it. Three tokens carry it: `--yeti-accordion-surface` for the set and so for the panels, `--yeti-accordion-summary` for a row at rest, and `--yeti-accordion-summary-hover` for the one under the pointer.
The last two are mixed from the first toward the text color rather than picked off the surface ramp, because the ramp's own steps are too small to read here. Mixing toward text also moves the right way by itself in both schemes, darkening the row on a light page and lifting it on a dark one, and a theme that restyles `--yeti-color-surface` or `--yeti-color-text` gets a header that still works without redefining anything.
The panel grows open rather than appearing. A `details` cannot animate its own height to `auto` in every engine, because the keywords that make `auto` interpolable are Chromium's alone, so the base layer makes every `details` a two-row grid instead: the summary takes the first row and the generated panel the second, and the transition runs on that row's size. That works in all three engines, and the panel clips its overflow so the content is revealed rather than squashed. The rows are packed to the start of the grid, because an `fr` row is a share of the leftover space and the summary would otherwise take part in that sum and visibly swell while the share was being interpolated. That rule is in the base, not here, which is why a lone `details` on a page with no classes opens exactly the same way.
Closing animates in Chromium and Firefox. WebKit shuts the panel at once, because it drops the panel's content before the row has anything left to shrink. Opening is smooth everywhere, which is the direction people watch.
Under `prefers-reduced-motion` the whole thing collapses with the rest of Yeti's motion, and the panel simply appears.
To open one at a time, give every `details` the same `name`. That is the browser's own exclusive accordion, and it needs nothing from Yeti. There is deliberately no Yeti attribute for it: CSS cannot set an attribute, so the only way to offer one would be a script, and this is a component that should never need one.
```html
Monthly
Cancel whenever you like.
Yearly
Two months free.
```
### Accessibility
A `summary` is a button to assistive tech, and its panel's state is announced as it opens and closes, so nothing needs ARIA. `summary` is not a heading, so if the rows are section titles, put a heading element inside each one. Keep the summary's text meaningful on its own, since a reader may hear the rows in a list before choosing one.
Children:
- > details: at least 2. One disclosure each. Give them all the same name attribute to have the browser close the others when one opens.
- summary: at least 2. The row that opens its panel; one per details, first inside it.
Tokens:
- --yeti-accordion-border — The line between panels and around the set.
- --yeti-accordion-padding — Padding of a summary and its panel.
- --yeti-accordion-surface — Background of the set, and so of an open panel.
- --yeti-accordion-summary — Background of a summary row, set apart from the panel below it.
- --yeti-accordion-summary-hover — Background of a summary under the pointer.
- --yeti-border-width — Width of those lines.
- --yeti-radius-md — Corner of the set.
Accessibility contract:
- Tab: Moves to the next summary.
- Enter / Space: Opens or closes the panel under the focused summary.
This is native: details is a disclosure, summary is its button, and the open state is the open attribute, all announced without help. Put a heading inside the summary if the row is a section title, since summary is not a heading by itself. To open one at a time, give every details the same name attribute, which is the browser's own exclusive accordion; Yeti adds no attribute for it, because CSS cannot set one and script should not be needed.
## affix (component, class="affix")
Joins a control with what belongs beside it, a unit, a symbol, a button, or another control, into one thing with a shared border and height.
### When to use it
A price with its currency, a distance with its unit, a search box with its button, a URL with a fixed domain after it, a country code beside a phone number. Whatever is joined shares one border and one height, so the parts read as one thing. A row of buttons joined the same way is `buttons` with `data-affix`.
### How it works
A flex row. Every control grows; a `span` becomes a labelled box on the sunken surface; a `button` keeps its own colors. Inner corners are squared and each border overlaps the next by one width, so the seam is a single line. The group sets no size of its own: it reads the field around it, so `data-size` on the field scales the whole row. A focused member rises above its neighbours and draws its focus ring inside its own border rather than outside it, where the ring would cross the seam onto the next member; a text control shows that ring on a click as well as on a Tab, by the browser's own rule for `:focus-visible`.
```html
.foundationcss.com
```
### Accessibility
The prefix is not part of the control, so a screen reader will not read it with the value unless you connect them: give the `span` an id and put it in the control's `aria-describedby`, or include the unit in the label ("Price in dollars"). The field's label still points at one control with `for`; when the affix joins two controls, the other one needs its own `aria-label`.
```html
```
Children:
- > *: at least 2. The controls and their attachments, in visual order.
- > input: at least 0. A control; every control grows to share the row.
- > select: at least 0. A control; every control grows to share the row.
- > span: at least 0, at most 2. A text prefix or suffix.
- > .button: at least 0, at most 2. A button prefix or suffix.
Tokens:
- --yeti-control-radius — Outer corners.
- --yeti-control-border — Border of the attachments.
- --yeti-border-width — The width the members overlap by.
- --yeti-space-sm — Inline padding of an attachment.
- --yeti-text-md — Text size of an attachment.
- --yeti-color-text-muted — Text of an attachment.
- --yeti-color-surface-sunken — Background of an attachment.
Accessibility contract:
A prefix or suffix that carries meaning (a currency, a unit) must reach the control's accessible name or description: give the span an id and add it to the control's aria-describedby, or say it in the label. When the affix holds more than one control, the field's label names one of them and each other control needs its own aria-label. A button in the affix is an ordinary button. Usually the affix is the control slot of a field.
## alert (component, class="alert")
A message in a hue: a tinted box with a colored edge, an optional icon, and an optional close button that the module wires up.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — Which hue: success for done, warning for careful, danger for wrong (alert is the same ladder under its Foundation 6 name), primary for news.
- data-emphasis: high | medium (default) | low — How loud: medium is a tint, high a solid fill, low the border alone.
On children:
- data-close on > button (boolean) — The close button; removes the alert when alert.js is loaded.
module: alert.js (optional)
- event yeti:close — Dispatched on the .alert after the fade and before it is removed, while it is still in the page.
### When to use it
Something the reader should know now: the form was saved, the trial ends on Friday, the address did not validate. One message, one hue, in the flow of the page where it applies. For a message that arrives and leaves on its own, wait; that is not this.
### How it works
A flex row: an optional icon, the message, and an optional close button pushed to the end. The hue draws the border and, at `medium` emphasis, a tint behind the text; `high` fills the box for the message that must be seen; `low` keeps the border alone. The start edge is `--yeti-alert-edge` thick, four border widths by default (the same mark the card uses for a variant), at every emphasis. The token is undeclared and read with that fallback, so the four is four of whatever `--yeti-border-width` is in force where the alert sits, not the page's root value; set `--yeti-alert-edge` directly to fix the width regardless.
The close button is a `button` carrying `data-close`. Yeti's `alert.js`, loaded once with `
```
This module is a stopgap and says so. The `interestfor` attribute is this feature standardised, and it is in one engine today. When it reaches Baseline the module goes and the attribute maps to it instead.
### Accessibility
The items are ordinary links and buttons, on purpose. `role="menu"` and `role="menuitem"` promise a full application menu, arrow keys and all, and a half-built one is worse than none; a disclosure panel of links is announced clearly by every screen reader and needs no script. Give the trigger a name, and let the browser handle the expanded state rather than setting `aria-expanded` yourself. Escape closes the panel and returns focus to the trigger, which is the browser's doing.
Opening on hover changes none of that. The module never touches focus, ARIA, or the keyboard; it only opens and closes the same popover the button already opens, and a reader who cannot use a pointer is unaffected either way.
Children:
- > button[popovertarget]: at least 1, at most 1. The trigger; its popovertarget names the panel's id.
- > [popover]: at least 1, at most 1. The panel, holding ordinary links and buttons.
Tokens:
- --yeti-dropdown-surface — Background of the panel.
- --yeti-dropdown-radius — Corner of the panel.
- --yeti-dropdown-padding — Padding around the items.
- --yeti-dropdown-min — Least width of the panel.
- --yeti-shadow-md — The panel's shadow.
- --yeti-control-size — Least height of an item.
- --yeti-dropdown-open-delay — How long the pointer must rest before a hover-triggered panel opens.
- --yeti-dropdown-close-delay — How long a hover-triggered panel waits after the pointer leaves before closing.
- --yeti-color-text — The panel's text.
- --yeti-duration-fast — How long the panel takes to open.
- --yeti-ease — The curve of that transition.
- --yeti-space-xs — Gap between an item's icon and its label, and between the trigger and the panel.
- --yeti-space-sm — Inline padding of an item.
- --yeti-color-surface-sunken — Background of an item under the pointer.
Accessibility contract:
- Enter / Space: On the trigger, opens or closes the panel.
- Tab: Walks the items, which are ordinary links and buttons.
- Escape: Closes the panel and returns focus to the trigger.
With data-trigger="hover" the module adds opening under the pointer and changes nothing else: the click, the keyboard, focus and the expanded state are all still the browser's, and a pointer that cannot hover never gets it. The trigger needs a name, and the browser sets its expanded state from popovertarget. The items are deliberately ordinary links and buttons with no menu roles: role="menu" promises arrow-key semantics that belong to application menus, and on a website a disclosure panel is both simpler and better announced. Inside a nav, put the dropdown in the li beside the link it belongs to.
## field (component, class="field")
One form control with its label, an optional hint, and an error that shows itself when the control is invalid.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — The color of a checked checkbox or radio.
- data-size: sm | md (default) | lg — Scales the control's height and text.
- data-inline (boolean) — Put the label beside the control. Checkboxes and radios are inline without it.
On children:
- data-hint on > * (boolean) — Help text, referenced by the control's aria-describedby.
- data-error on > * (boolean) — The error message, hidden until the control is invalid.
module: range.js (optional)
module: validate.js (optional)
- event yeti:invalid with detail { controls } — Dispatched on the form when a submit is refused, carrying the invalid controls in document order.
### When to use it
Every control in a form: text, email, number, select, textarea, checkbox, radio, switch, range. A form is a `stack` of fields and a button; the field owns what bare HTML cannot, the label's link to its control, the help text, and the error.
### How it works
A tight column: label, control, hint, error. The control is a native element styled to the control tokens, so a theme that changes `--yeti-control-radius` changes every input. The error is hidden until the control is invalid *and* the visitor has touched it (`:user-invalid`), or until you set `aria-invalid="true"` after a server round trip; then it shows and the border turns to the alert color. A `required` control gets a marker after its label. A checkbox or radio is laid out inline automatically, label after the control, and its checked mark is a variant-colored centre inside a ring of the surface color.
```html
```
A checkbox with `role="switch"` becomes a switch: a track with a thumb that slides to the end and takes the field's color when on. Off, the track is `--yeti-switch-track` and the thumb `--yeti-switch-thumb`, the control border and surface by default; a theme that darkens the control border sets these so off stays distinct from on, and the off track must still clear 3:1 against the page surface. A `range` input gets a thin track and a round thumb in the field's color, the height of a control so it is easy to grab.
The filled part of the track is `--yeti-range-value`, how far along the value sits from 0 to 1, because CSS cannot read an input's value. `range.js` keeps it in step, and writes the value into an `output` placed before the input, which is drawn over the thumb. Give the output `aria-hidden`: the input announces its own value and a screen reader should not hear it twice. Leave the output out of a page that does not load the module, or it stays empty.
The fill stops where the thumb's centre is, not at that share of the width. A thumb's centre only travels from half a thumb in to half a thumb from the end, so a plain percentage runs ahead of it, by eleven pixels at each end on a track this wide. The stylesheet makes that correction, because the thumb's width is an em it already knows and script would have to measure it again on every resize.
Without the module the track sits wherever `--yeti-range-value` says, so set it on the input for a static one.
```html
```
```html
```
Set by hand, it is a number rather than a percentage: `style="--yeti-range-value: 0.7"`.
`validate.js` is the field's second module, and it is about the form rather than one control. On submit it finds every invalid control in the form, inside a `.field` or not, and sets `aria-invalid="true"` on each — which is the same attribute a server round trip would set, so the error shows and the border turns at once — writes the browser's own message into the empty `[data-error]` of the nearest field that has one, focuses the first control, and stops the submission. A control outside every field is counted and stops the submit like any other; there is simply nowhere to put its message, and under `novalidate` nothing else was going to stop it. A radio or checkbox group gets its message on the `fieldset.field` that carries the slot, not on the bare `.field` around each input. It dispatches `yeti:invalid` on the form with the controls it found, so a page can count them, scroll a summary into view, or send them somewhere. Typing or picking a valid value clears the mark again.
Give a form that loads it `novalidate`. Without that attribute the browser opens its own bubble on the first invalid control and never fires a submit event at all, so the module never runs and the page gets the bubble instead of its own error text. An error element you fill in yourself is never overwritten; an empty one is the slot the module writes into.
```html
```
### Accessibility
The label must point at the control with `for` and the control must carry that `id`; Yeti's validator refuses an example without the pair. Put the hint's and the error's ids in the control's `aria-describedby`, so a screen reader hears the help text with the control and the error the moment it appears. Errors found on the server are shown with `aria-invalid="true"`. The required marker is a visual echo of the `required` attribute, which is what is announced.
Children:
- > label: at least 0, at most 1. The label, with for pointing at the control's id. Required unless the field is a fieldset with a legend.
- > legend: at least 0, at most 1. The legend, when the field is a fieldset grouping several controls.
- > input: at least 0, at most 1. The control.
- > select: at least 0, at most 1. The control.
- > textarea: at least 0, at most 1. The control.
- > .affix: at least 0, at most 1. The control slot as an affix: a control with attachments, or two controls joined.
- > output: at least 0, at most 1. A range's value, written by range.js and drawn over the thumb. Put it before the input. Leave it out on a page that does not load the module, which would leave it empty.
- > [data-hint]: at least 0, at most 1. Help text, referenced by the control's aria-describedby.
- > [data-error]: at least 0, at most 1. The error message, hidden until the control is invalid.
Tokens:
- --yeti-field-gap — Space between label, control, and hint.
- --yeti-control-size — Minimum height of the control.
- --yeti-control-radius — Corner of the control.
- --yeti-control-border — Border of the control at rest.
- --yeti-switch-track — Fill and border of an off switch's track.
- --yeti-switch-thumb — The thumb of an off switch.
- --yeti-range-value — How far along a range's track the value sits, 0 to 1. range.js sets it; set it on the input yourself for a static one.
- --yeti-control-surface — Background of the control.
- --yeti-control-chevron — The select's chevron image.
- --yeti-color-alert — Border of an invalid control.
- --yeti-color-primary — The default variant's color, when data-variant is absent.
- --yeti-color-primary-subtle — The default variant's tint.
- --yeti-color-primary-soft — The default variant's soft stop.
- --yeti-color-primary-strong — The default variant's strong stop.
- --yeti-color-primary-text — The default variant's text color.
- --yeti-on-primary — Text on the default variant's color.
- --yeti-text-md — Text size when data-size is absent.
- --yeti-space-sm — The space step when data-size is absent; a fieldset's padding follows it.
- --yeti-weight-strong — Weight of the label or legend.
- --yeti-color-text — Text of the control.
- --yeti-border-width — Border width of every control, and of a fieldset.
- --yeti-duration-fast — How long a control's border and a switch take to change.
- --yeti-ease — The curve of that transition.
- --yeti-color-border-strong — Border of a focused control.
- --yeti-radius-full — Corners of the switch and of the range track.
- --yeti-space-xs — Gap of an inline field, and a legend's inline padding.
- --yeti-text-sm — Text size of the hint and the error.
- --yeti-color-text-muted — The hint.
- --yeti-color-alert-text — The error, and the required marker.
- --yeti-space-md — Padding of a fieldset.
Accessibility contract:
The label's for must match the control's id; the validator checks it. Reference the hint and the error from the control with aria-describedby so both are announced. Use aria-invalid="true" for errors found on the server. The required marker is decoration; the required attribute is what assistive tech reads. A switch is a checkbox with role="switch"; its label reads as the switch's name. A range needs a label like any control, and aria-valuetext when the numbers are not what a person would say. With validate.js loaded, the same aria-invalid is set from the browser's own check when a submit is refused, and the browser's message is written into an empty [data-error].
## nav (component, class="nav")
A bar with a brand, a list of links, and actions, that collapses the links behind a toggle at its own width and opens them as a sheet, a drawer, or a screen.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — The hue of the current link and of the hover tint.
- data-threshold: 3xs | 2xs | xs | sm | md (default) | lg | xl | 2xl — The nav's own width at or above which the links sit in the bar; below it they are behind the toggle.
- data-panel: sheet (default) | drawer | screen — The shape of the open list below the threshold: a sheet under the bar, a drawer from the start edge, or the whole screen.
- data-gap: none | xs | sm (default) | md | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between the bar's parts and between the links.
- data-sticky (boolean) — Keep the bar at the top of the scrollport as the page scrolls, at --yeti-sticky-offset from the edge.
On children:
- data-brand on > * (boolean) — The site's name or mark, usually a link home.
- data-close on li (boolean) — The item holding the button that closes the panel; hidden in the bar.
- data-actions on > * (boolean) — Buttons at the end of the bar.
### When to use it
The bar across the top of a site: the name, the main links, a button or two. It is a bar of links, not a menu system. Sections with their own submenus are the dropdown's job, and a column of links down the side of a page is a `stack` inside a `sidebar`, which needs no component.
### How it works
One list of links, two modes. The list carries `popover` and the button carries `popovertarget` naming it. When the bar is narrower than `data-threshold` (its own width, not the screen's, so a nav in a narrow column collapses while the same one across a page does not), the list is a closed popover and the button opens it; light dismiss, Escape, and the button's expanded state all come from the browser. At the threshold and above, the nav's own container query puts the list back in the bar and hides the button. The `popover` attribute is still there, so nothing changes in the markup and no link exists twice.
`data-panel` chooses the shape of the open list. `sheet`, the default, hangs under the bar at the full width. `drawer` slides in from the start edge as a column with the page dimmed behind it. `screen` fills the viewport with the links large and centred. Where anchor positioning exists the sheet sits exactly under the bar; elsewhere it starts at the top of the viewport and covers the bar, and Escape or a click outside still closes it. A close item, an `li` carrying `data-close` with a button that hides the popover, sits in the panel's top end corner and disappears in the bar; `screen` needs one, since there is no outside to click.
```html
```
A panel that is open stays a panel until it is dismissed, whatever the nav's width; widening past the threshold does not return the links to the bar on its own. Escape or a click outside closes the panel, and only then do the links return to the bar. `data-threshold` is the attribute `columns` uses, so a nav and the columns under it can switch at the same width.
A nav item may hold a `dropdown` for a section with children of its own: the item becomes a button rather than a link, since the dropdown brings its own trigger. Its panel is in the top layer, so the bar's own containment cannot clip it.
The panel changes shape with the nav. In the bar it is the dropdown's usual card, anchored under its trigger. Once the links are behind the toggle it becomes a full-width block instead, docked to the foot of its trigger and squared off against the panel's own edges, so a sheet, a drawer and a screen each keep their submenu inside them. It is still a popover, so it covers the items below rather than pushing them down, and Escape or a click outside closes it before the panel it sits in. Where anchor positioning is missing the block docks to the foot of the viewport, which is still full width and still within reach.
`data-sticky` keeps the bar at the top of the scrollport as the page scrolls, `--yeti-sticky-offset` from the edge. Everything else on the page that sticks stops at that same offset and would slide under the bar, so set the token to the bar's own height plus the gap you want on whatever holds them — the shell's body row, say — and leave the nav itself reading the default.
Links that point down the same page (`href="#pricing"`) turn on smooth scrolling for the page, through `--yeti-toc-scroll`, the same switch a `toc` uses; a bare `href="#"` placeholder does not count, and under reduced motion the page goes back to the browser's jump.
The brand is a wordmark, so it has tokens of its own: `--yeti-nav-brand-weight` and `--yeti-nav-brand-size`. Plain links take `--yeti-nav-link`, the bar's own color by default; on a painted bar set it to the color that reads there. The current link and a hovered link keep the variant's tints, and the open panel resets it to its own text color. A long wordmark in a narrow bar breaks into two balanced lines rather than pushing the bar past its edge.
The actions slot takes anything, not only buttons: a running head, a version, a phone number. It inherits the bar's type, so quiet text is the page's two declarations:
```css
.nav > [data-actions] { font-size: var(--yeti-text-sm); color: var(--yeti-color-text-muted); }
```
### Accessibility
Give the `nav` an `aria-label`, since a page often has more than one. The toggle needs a name, from `aria-label` or visible text; the browser sets its expanded state. Mark the current page's link with `aria-current="page"`. Put `role="list"` on the `ul`: Yeti's reset only removes list markers where that role says the list is decorative. The open panel follows the toggle in the tab order, so Tab from the toggle reaches the first link; Escape closes the panel and returns focus to the toggle.
Children:
- > [data-brand]: at least 0, at most 1. The site's name or mark, usually a link home.
- > button[popovertarget]: at least 1, at most 1. The toggle; its popovertarget names the list's id. Hidden while the links are in the bar.
- > ul[popover]: at least 1, at most 1. The links, one per li, with role="list" and the id the toggle names.
- li: at least 1. An entry in the list; each holds one link.
- [data-close]: at least 0, at most 1. An li holding a button with popovertargetaction="hide", shown at the panel's top end corner and hidden in the bar. Required with data-panel="screen".
- .dropdown: at least 0. A nav item may hold a dropdown for its own children; the panel is in the top layer, so the bar never clips it. In the bar it is the usual card; behind the toggle it becomes a full-width block docked under its trigger and squared off against the open panel's edges.
- > [data-actions]: at least 0, at most 1. Buttons at the end of the bar.
Tokens:
- --yeti-nav-padding — Padding of the bar and of the open panel.
- --yeti-nav-radius — Corner of the links and the toggle.
- --yeti-nav-border — The bar's bottom edge, and the sheet's.
- --yeti-nav-surface — Background of the bar.
- --yeti-nav-panel — Background of the open panel.
- --yeti-nav-brand-weight — Weight of the brand.
- --yeti-nav-brand-size — Size of the brand; 1em keeps the bar's.
- --yeti-nav-link — Color of a plain link in the bar; the bar's own color unless set. Current and hover keep the variant's tints, and the open panel resets it to its own text color.
- --yeti-stretch-small — font-stretch of a link.
- --yeti-control-size — Minimum height of the bar, the toggle, and each link.
- --yeti-shadow-md — The open panel's shadow.
- --yeti-shadow-color — The drawer's backdrop.
- --yeti-color-text — The bar's text, and the open panel's.
- --yeti-border-width — Width of the bar's bottom edge, and of the sheet's.
- --yeti-space-sm — The default gap; a link's inline padding and the close item's inset follow it.
- --yeti-color-primary — The default variant's color, when data-variant is absent.
- --yeti-color-primary-subtle — The default variant's tint.
- --yeti-color-primary-soft — The default variant's soft stop.
- --yeti-color-primary-strong — The default variant's strong stop.
- --yeti-color-primary-text — The default variant's text color.
- --yeti-on-primary — Text on the default variant's color.
- --yeti-weight-strong — Weight of the brand, and of the current page's link.
- --yeti-space-xs — Block padding of a link, and of the close button.
- --yeti-duration-fast — How long the panel takes to open.
- --yeti-ease — The curve of that transition.
- --yeti-text-lg — Text size of the links in a screen panel.
- --yeti-space-xl — Inset of a screen panel that has no close item.
Accessibility contract:
Required: aria-label | aria-labelledby
- Enter / Space: On the toggle, opens or closes the list.
- Tab: From the toggle, into the open list's first link.
- Escape: Closes the open list and returns focus to the toggle.
Give the nav an aria-label, since a page often has more than one. The toggle needs a name (aria-label or visible text); the browser sets its expanded state. Mark the current page's link with aria-current="page". Put role="list" on the ul, because the reset only removes list markers where that role says the list is decorative. With data-panel="screen" there is no outside to click, so include the close item.
## pagination (component, class="pagination")
Links to the pages of a long list, each a square target, the current one filled, shrinking to Previous, the current page, and Next when it is narrow.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — The hue of the current page and of the hover tint.
- data-size: sm | md (default) | lg — The text step.
- data-threshold: 3xs | 2xs | xs | sm (default) | md | lg | xl | 2xl — The pagination's own width below which only Previous, the current page, and Next remain.
- data-justify: start (default) | center | end | between | around | evenly — Where the links sit in the row.
### When to use it
Under a list that is split into pages: search results, an archive, a table too long for one screen. It is for pages a reader may want to jump between; a feed that only ever goes forward needs one Next link, not this.
### How it works
A row of links, each at least a control's width and height so it is easy to hit, the current page filled with the hue. An ellipsis in a `span` stands for skipped pages. Below `data-threshold`, the pagination's own width, everything but Previous, the current page, and Next is hidden, so the same markup is a full row in a wide column and three targets in a narrow one, with no breakpoint. `data-justify` places the row.
```html
```
### Accessibility
Label the `nav` `aria-label="Pagination"`. The current page's link carries `aria-current="page"`, which is also what the fill follows. Previous and Next carry `rel="prev"` and `rel="next"` with visible text, or an `aria-label` if they are icons; they are what remains when the row is narrow, so they must make sense alone. The ellipsis is a `span`, not a link. `role="list"` on the `ol` keeps it a list where the reset removes the markers.
Children:
- > ol: at least 1, at most 1. The links, in page order, with role="list".
- li: at least 2. A page link, or a span holding an ellipsis for skipped pages.
Tokens:
- --yeti-pagination-radius — Corner of each target.
- --yeti-control-size — Minimum size of each target.
- --yeti-color-primary — The default variant's color, when data-variant is absent.
- --yeti-color-primary-subtle — The default variant's tint.
- --yeti-color-primary-soft — The default variant's soft stop.
- --yeti-color-primary-strong — The default variant's strong stop.
- --yeti-color-primary-text — The default variant's text color.
- --yeti-on-primary — Text on the default variant's color.
- --yeti-text-md — Text size when data-size is absent.
- --yeti-space-sm — The space step when data-size is absent; a target's inline padding follows it.
- --yeti-space-xs — Gap between targets.
- --yeti-color-text-muted — The span standing for skipped pages.
Accessibility contract:
Required: aria-label | aria-labelledby
Put aria-label="Pagination" on the nav. The current page's link carries aria-current="page". Previous and Next carry rel="prev" and rel="next" and visible text or an aria-label; they are what remains when the row is narrow. Skipped pages are a span, not a link. Put role="list" on the ol.
## progress (component, class="progress")
The native progress element as a thin bar in a hue, filled to its value, or striped and moving when it has none.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — The hue of the filled part.
- data-size: sm | md (default) | lg — The bar's thickness: half the size's space step.
- data-scroll (boolean) — Fill with how far the nearest scroll container has been scrolled, the page when the bar is at its top, instead of with a value: a reading-progress bar. On a block with the class, not a progress element, and hidden from assistive tech; where a scroll timeline is unsupported the bar is not shown.
### When to use it
Work with a known length: an upload, a wizard's steps, a quota. Leave the value off for work whose length is unknown and the bar says "busy" instead of "40%".
### How it works
The native `progress` element, with its own drawing switched off and a thin rounded track in its place. The filled part is the hue, through the engines' own pseudo-elements, and slides when the value changes. Without a `value` the element is indeterminate: the track takes diagonal stripes that move along it. `data-size` sets the thickness to half of the size's space step.
```html
```
### Reading progress
`data-scroll` makes the bar fill with how far the nearest scroll container has been scrolled: the page, when the bar sits at its top, or a `scroller`, when the bar is inside one. It is the one form of progress that is not a `progress` element, because there is no value to announce: it is a block with the class, and it carries `aria-hidden` so a reader is not told about a bar that only mirrors their own scrolling. Pin it with `data-sticky` as the first child of the page's stack.
```html
…
```
It is guarded by `@supports`, and where a scroll timeline is missing the bar is not shown at all: a bar that never moved would say the reader had not started. It is not switched off under reduced motion, because nothing in it moves on its own; the fill follows the reader's hand and stops when they do.
A reading bar at the top of the page usually wants to sit on the edge and be thin: set `--yeti-sticky-offset: 0` and `--yeti-progress-size: 2px` on the bar itself. It paints above the page as it scrolls.
### Accessibility
A `progress` element is a progress bar to assistive tech already; it needs a name, from `aria-label` or `aria-labelledby`. Keep the text between the tags current, since some readers announce that rather than the value. An indeterminate bar is announced as busy with no percentage, which is right.
Tokens:
- --yeti-progress-radius — Corner of the bar and of its value.
- --yeti-progress-size — Thickness of the bar; overrides the size step.
- --yeti-color-surface-sunken — The track.
- --yeti-color-primary — The default variant's color, when data-variant is absent.
- --yeti-color-primary-subtle — The default variant's tint.
- --yeti-color-primary-soft — The default variant's soft stop.
- --yeti-color-primary-strong — The default variant's strong stop.
- --yeti-color-primary-text — The default variant's text color.
- --yeti-on-primary — Text on the default variant's color.
- --yeti-text-md — The text step when data-size is absent.
- --yeti-space-sm — The space step when data-size is absent; the bar's height follows it.
- --yeti-duration-base — How long the value takes to move; one cycle of the indeterminate sweep is four of it.
- --yeti-ease — The curve of the value's move.
- --yeti-motion-iterations — How many times the indeterminate sweep repeats; one under reduced motion.
Accessibility contract:
Required: aria-label | aria-labelledby | aria-hidden
Put the class on a progress element and give it a name with aria-label or aria-labelledby. Keep its text content current ("40%"), since older assistive tech reads that. Leave out value for work whose length is unknown; the element is then indeterminate and says so. A reading-progress bar has no value to announce, so it carries aria-hidden instead of a name; the rule accepts either.
## seam (component, class="seam")
Gives a section a shaped edge, a slant, a curve, or a wave, cut from its own background so whatever is behind shows through.
Attributes:
- data-shape: slant (default) | curve | wave — The shape of the cut edge.
- data-size: sm | md (default) | lg — The depth of the cut: twice the size's space step.
- data-edge: top | bottom (default) | both — Which edge is cut.
- data-flip (boolean) — Mirror the shape horizontally, for alternating sections. A curve is symmetric and does not change.
### When to use it
Between bands of a landing page, where a straight line between two backgrounds looks cut from cardboard. A slant, a curve, or a wave along one edge of a section, alternating direction down the page, is the classic move.
### How it works
The cut is taken from the section itself, not added on top: a slant is a `clip-path` polygon, a curve or wave a mask made of a solid body and a shaped strip along the cut edge. Because the section's own background is what is cut, the page, the previous section, or an image behind it shows through, with no extra element. `data-edge` picks the edge, `data-size` the depth, `data-flip` mirrors a slant or a wave for the next section down. The seam adds the depth of the cut as space after or before the content, so give the section its padding as usual and nothing sits in the cut.
```html
Slanted top and bottom
```
### Accessibility
Purely visual. The clip and the mask change nothing about the content, its order, or its size for assistive tech; the added space keeps text out of the cut for everyone.
Tokens:
- --yeti-seam-size — Fix the depth for every size.
- --yeti-space-sm — The depth when data-size is absent.
Accessibility contract:
Decorative only; nothing changes for assistive tech. The seam adds its depth as space on a cut edge so text never sits in the cut.
## spinner (component, class="spinner")
A turning ring in a hue, the size of the text, for waiting with no known end; a busy button shows the same ring after its label.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — The hue of the bright edge.
- data-size: sm | md (default) | lg — The ring is one em of the size's text step.
### When to use it
A wait with no known end: a page section still fetching, a search running. When the length is known, use `progress`. When the wait belongs to a button, set `aria-busy="true"` on the button and the ring appears there by itself.
### How it works
A one-em ring: a border in the hue's subtle step with the top edge in the hue, turning once per `--yeti-spinner-duration`. `data-size` sets the em through the text step, so a spinner sits in a line of text at that text's size. A button with `aria-busy="true"` draws the same ring after its label from its own text color, so it reads on a filled button and an outlined one alike.
```html
```
### Accessibility
Standing alone, give the spinner `role="status"` and an `aria-label` such as "Loading", so the wait is announced once and not again. Inside a busy button it needs nothing more: `aria-busy` on the button says it. Under reduced motion every animation duration collapses, so the ring stands still; a still ring with one bright edge still reads as "waiting".
Tokens:
- --yeti-spinner-duration — One turn.
- --yeti-spinner-width — Thickness of the ring.
- --yeti-motion-iterations — How many times the ring turns; one under reduced motion.
- --yeti-color-primary — The default variant's color, when data-variant is absent.
- --yeti-color-primary-subtle — The default variant's tint.
- --yeti-color-primary-soft — The default variant's soft stop.
- --yeti-color-primary-strong — The default variant's strong stop.
- --yeti-color-primary-text — The default variant's text color.
- --yeti-on-primary — Text on the default variant's color.
- --yeti-text-md — The text step when data-size is absent; the ring's diameter follows it.
- --yeti-space-sm — The space step when data-size is absent.
Accessibility contract:
Standing alone, give it role="status" and an aria-label such as "Loading", so the wait is announced once. Inside a button that carries aria-busy="true" it needs nothing: the button already says it is busy, and the ring appears by itself. Under reduced motion the ring stands still.
## table (component, class="table")
A data table styled in place: header rule, row lines, optional stripes, hover, and cell borders, with numeric columns that line up.
Attributes:
- data-size: sm | md (default) | lg — Cell padding and text size; sm is the compact table.
- data-striped (boolean) — Tint every other body row.
- data-hover (boolean) — Tint the row under the pointer.
- data-border (boolean) — Borders around every cell instead of lines between rows, as a box draws its border.
- data-nowrap (boolean) — Every cell keeps its line; put the table in a scroller.
- data-fixed (boolean) — Fixed table layout: the columns share the width equally, whatever their content. A width on a first-row header cell makes that column wider and the rest share what is left.
On children:
- data-numeric on * (boolean) — Tabular figures, so digits line up, on any element; on a table's cells and headers the column is end-aligned too.
- data-nowrap on th, td (boolean) — This cell keeps its line.
- data-align on th, td, tr: start | center | end — Aligns the text of this cell, or of every cell in this row. A cell's own data-align or data-numeric outranks its row's.
### When to use it
Rows and columns of data: prices, results, comparisons. If the content is a list of things with a picture each, that is a `grid` of cards, not a table.
### How it works
The class goes on the `table`. Cells get padding from `data-size` and a line beneath each row; the header row gets a stronger rule. `data-striped` tints every other row, `data-hover` the row under the pointer, `data-border` draws every cell. Mark a column of numbers with `data-numeric` on its cells and header: they align to the end with tabular figures, so the digits line up. The marker works on any element, a price or a legend's values, where it gives the figures alone. A comparison, a feature column and a column per plan, reads across best when the columns are equal: `data-fixed` gives every column the same share of the width, whatever it holds. To make the first column wider, give its header cell a width, `
`, and the others share the rest. `data-align="center"` (or `start`, `end`) on a `th` or `td` aligns that cell, and on a `tr` every cell in the row; a cell's own value, or its `data-numeric`, outranks its row's. A tick in a centered cell is an inline `svg`, and the reset makes an `svg` a block, so wrap it in an `icon` or give it `display: inline-block` for the alignment to reach it.
```html
Feature
Free
Walker
Offline maps
No
Yes
```
`data-sticky` on the `thead` pins the header row at `--yeti-sticky-offset` while the rows scroll under it; its cells take the surface color so the rows do not show through, and the rule beneath them moves with them. Set the offset to `0` on the `thead` to pin it flush with the top. A table that is wider than its container goes inside a `scroller`, which scrolls it sideways while the table itself stays a table. `data-nowrap` on the table keeps every cell's line; on a single `th` or `td` it keeps just that cell's; either way, put the table in a scroller so the kept line has somewhere to go.
```html
Quarterly results
Region
Q1
Q2
North
120
132
```
### Accessibility
A table needs a name: a `caption`, or `aria-label` or `aria-labelledby` on the element. Header cells carry `scope`. Yeti never turns a table into stacked cards at narrow widths, because that breaks the relationships a screen reader depends on; use a `scroller` and keep the table a table.
Children:
- > caption: at least 0, at most 1. The table's title.
- > thead: at least 0, at most 1. The header row. With data-sticky it pins at --yeti-sticky-offset as the rows scroll under it, painted with the surface and carrying its rule.
- > tbody: at least 1. The rows.
- [data-numeric]: at least 0. A cell (or header cell) of numbers: end-aligned with tabular figures.
Tokens:
- --yeti-table-stripe — Fill of even rows with data-striped.
- --yeti-table-border — Row and cell borders.
- --yeti-color-border-strong — The rule under the header.
- --yeti-text-md — Text size when data-size is absent.
- --yeti-space-sm — The space step when data-size is absent; a cell's inline padding follows it.
- --yeti-weight-strong — Weight of the caption and of header cells.
- --yeti-border-width — Width of the row rules, and of every cell border with data-border.
- --yeti-color-surface — Background of the head cells when the head is pinned with data-sticky.
- --yeti-color-surface-sunken — Background of a row under the pointer with data-hover.
Accessibility contract:
Give the table a caption, or aria-label / aria-labelledby. Header cells carry scope="col" or scope="row". A table wider than its container goes inside a scroller with tabindex="0" and a label; the table itself never changes shape, so it stays a table for assistive tech.
## tabs (component, class="tabs")
A row of tabs over their panels, showing one at a time when its module is loaded and all of them when it is not.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — The hue of the selected tab and its edge.
- data-orientation: horizontal (default) | vertical — Whether the tab list runs along the top or down the side.
- data-gap: none | xs | sm | md (default) | lg | xl | 2xl | 3xl | 4xl | xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-4xl | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-4xl | md-lg | md-xl | md-2xl | md-3xl | md-4xl | lg-xl | lg-2xl | lg-3xl | lg-4xl | xl-2xl | xl-3xl | xl-4xl | 2xl-3xl | 2xl-4xl | 3xl-4xl — Space between the list and the panels.
- data-emphasis: high — The selected tab is filled with the variant; without it the selected tab is underlined.
module: tabs.js (optional)
- event yeti:select with detail { tab, panel } — Dispatched on the .tabs when a click, an arrow key, or a hash reveal selects a tab; never for the pass at load that finds no hash to reveal.
### When to use it
Alternative views of one thing: a profile and its billing, a chart and its table. Not for steps in a sequence, which want a form, and not for hiding content a reader needs to compare against something else.
### How it works
A tab list of buttons over their panels. The CSS styles the list and marks the selected tab with the hue on its edge, and it never hides a panel. That is deliberate: a page that does not load `tabs.js`, or where the script fails, shows every panel under its own tab, so nothing a reader came for is locked away.
Load the module and it pairs every tab with the panel its `aria-controls` names, shows one, hides the rest, and takes over the keyboard: arrows move selection, Home and End jump to the ends, and only the selected tab is in the tab order, so Tab leaves the list rather than walking it. A link into a hidden panel opens its tab: on load and on every `hashchange` the module looks for the URL's fragment inside a panel and, if it finds one, selects that panel's tab and brings the target into view, without moving keyboard focus to the tab. Tabs may nest inside another tab's panel: each set keeps its own selection and its own arrow keys, and a link into the inner set opens every enclosing panel on the way. `data-emphasis="high"` fills the selected tab with the variant instead of underlining it, its top corners (or, in a vertical list, the corners away from the divider) rounded like any other filled control.
```html
Summary panel.
Detail panel.
```
```html
General settings.
Your API keys.
```
### Accessibility
Name the tablist, since a page may have more than one. Each tab's `aria-controls` names its panel and each panel's `aria-labelledby` names its tab; the module reads that pairing rather than guessing from document order, so the markup stays the source of truth. The module also maintains `aria-selected` and the roving `tabindex`, which is what makes a tab list feel like one control rather than a row of buttons. Without the module the markup is still valid: the roles are right and every panel is readable.
Children:
- > [role="tablist"]: at least 1, at most 1. The tabs, in order, with a name of its own.
- [role="tab"]: at least 2. A button whose aria-controls names its panel.
- > [role="tabpanel"]: at least 2. A panel whose aria-labelledby names its tab.
Tokens:
- --yeti-tabs-border — The line under or beside the list.
- --yeti-tabs-padding — Padding of one tab.
- --yeti-border-width — Width of that line; the selected mark is twice it.
- --yeti-space-md — The default gap between the list and the panel.
- --yeti-color-primary — The default variant's color, when data-variant is absent.
- --yeti-color-primary-subtle — The default variant's tint.
- --yeti-color-primary-soft — The default variant's soft stop.
- --yeti-color-primary-strong — The default variant's strong stop.
- --yeti-color-primary-text — The default variant's text color.
- --yeti-on-primary — Text on the default variant's color.
- --yeti-space-xs — Gap between tabs.
- --yeti-weight-strong — Weight of a tab's label.
- --yeti-duration-fast — How long a tab takes to change color.
- --yeti-ease — The curve of that transition.
- --yeti-radius-sm — Corner radius of the selected tab with data-emphasis="high".
Accessibility contract:
- Left / Right: Moves selection along a horizontal list.
- Up / Down: Moves selection along a vertical list.
- Home / End: Selects the first or the last tab.
- Tab: Leaves the list for the selected panel.
Give the tablist a name with aria-label or aria-labelledby. Every tab is a button whose aria-controls names its panel, and every panel names its tab back with aria-labelledby; that pairing is what the module reads. With tabs.js loaded one tab is selected, the rest take tabindex="-1" for a roving focus, and the inactive panels are hidden. Without it nothing is hidden and every tab is focusable, which is plainer but never traps content.
## toc (component, class="toc")
A column of links to the headings on the page, with the one being read marked as the reader scrolls.
Attributes:
- data-variant: primary (default) | secondary | success | warning | alert | danger | neutral | black | white — The hue of the current link and its edge bar.
- data-size: sm | md (default) | lg — The text step, and the inset of each link with it.
- data-numbered (boolean) — Number the entries before their links, nested ones as 2.1, in the muted color.
module: toc.js (optional)
- event yeti:current with detail { link, heading } — Dispatched on the .toc when the mark moves to another link.
### When to use it
A long page a reader scrolls rather than clicks through: a guide, a reference, a post with sections. The list says what is on the page and how far through it the reader is. A page with three short sections does not need one, and neither does a page whose headings would not fit down one side.
### How it works
A column of links, each pointing at the id of a heading on the same page. The current one carries `aria-current="true"` and takes the variant's color, a strong weight, and a bar down its start edge; the bar is on every link and transparent until then, so nothing shifts as the mark moves. `data-variant` picks the hue and `data-size` steps the text. A link under the pointer takes `--yeti-toc-hover`, read with a fallback to the variant's own subtle tint, so setting it to transparent turns the fill off without touching the variant.
`toc.js` moves the mark. It watches the linked headings with one `IntersectionObserver` and marks the link of whichever heading is topmost in view, dispatching `yeti:current` on the `.toc` with the link and the heading each time it moves. Between two headings nothing is in view, and the last mark stays rather than flickering off. Without the module the list is a working list of links, and a page that knows its own current section can simply ship the attribute in its HTML.
Following a link scrolls smoothly, from `--yeti-toc-scroll` applied to the page — the token collapses to `auto` under `prefers-reduced-motion`, which a plain `scroll-behavior` in this layer could not, since it would outrank the reset's own rule.
Each row's block padding is `--yeti-toc-padding`, a control's density by default; set it smaller on one `.toc` for a book's own contents page.
The list may itself be a `cluster` instead of a plain `ul`, for a row of contents rather than a column — a settings page's section list beside its narrow rail, say. The toc's own column and gap only apply to a list that is not a cluster, so the layout class wins and the row wraps as any cluster does.
`data-numbered` turns the list into a contents list. Every `ul` resets a CSS counter and every `li` counts, so a nested list picks up where its parent left off and reads `2.1` rather than starting over at `1`. The number sits before the link, in the muted color, with tabular figures, in a box wide enough for a two-figure count or a nested 2.1, so the link text lines up. A title that wraps hangs from the number: its second line starts under its first word, not under the digits.
```html
```
```html
```
### Accessibility
Label the `nav` — `aria-label="On this page"` — because a page with a toc has at least two navs and they have to be told apart. Put `role="list"` on the `ul`, which is what keeps it a list where the reset removes the markers. The current section's link carries `aria-current="true"`; the color and the bar are the visible half of the same fact, never the only half. The module owns that attribute on these links: it sets and clears `aria-current="true"` and strips any other `aria-current` value an author puts on a toc link. The links are ordinary same-page links, so the keyboard reaches them in order and Enter follows one with no script at all.
Children:
- > ul: at least 1, at most 1. The links, one per li, with role="list".
- li: at least 1. One entry, holding a link whose href is the id of a heading on this page.
Tokens:
- --yeti-toc-scroll — How the page scrolls to a heading; also read by a nav with in-page links.
- --yeti-toc-padding — The rows' block padding.
- --yeti-color-text-muted — A link that is not the current one.
- --yeti-color-text — A link under the pointer.
- --yeti-toc-hover — The hover fill; transparent turns it off.
- --yeti-space-xs — Gap between links, and their block padding.
- --yeti-space-sm — The space step when data-size is absent.
- --yeti-text-md — Text size when data-size is absent.
- --yeti-border-width — The edge bar is three of them.
- --yeti-weight-strong — Weight of the current link.
- --yeti-duration-fast — How long the mark takes to move.
- --yeti-ease — The curve of that transition.
- --yeti-color-primary — The default variant's color, when data-variant is absent.
- --yeti-color-primary-subtle — The default variant's hover tint.
- --yeti-color-primary-text — The default variant's text color.
Accessibility contract:
Required: aria-label | aria-labelledby
Give the nav an aria-label such as "On this page", since a page with a toc has at least two navs. Put role="list" on the ul, because the reset only removes list markers where that role says the list is decorative. The link for the section being read carries aria-current="true"; toc.js moves it, and a page that ships the mark in its HTML is correct without the module. The module owns aria-current on these links: it sets and clears the value "true" and strips any other aria-current an author puts on a toc link. Every link points at an id on this page, so the list works with no script at all.
## tooltip (component, class="tooltip")
A short hint that appears beside a control on hover and on focus, described to assistive tech and needing no script.
Attributes:
- data-placement: top (default) | bottom | start | end — Which side of the trigger the bubble sits on.
### When to use it
A few words about a control whose purpose is not obvious from its face, most often an icon-only button. Never for anything the reader has to have: there is no hover on a touch screen, and a tooltip that matters is a tooltip that will be missed.
### How it works
The component wraps a trigger and a bubble. The bubble is hidden until the trigger is hovered or reached by keyboard, which is why it needs no script: `:hover` and `:has(:focus-visible)` on the wrapper do the whole job. A click leaves the trigger focused too, but that focus is not the kind `:focus-visible` shows, so a mouse click opens nothing; only Tabbing to the trigger does. It is deliberately not a `popover`, because nothing but script can open one of those, and a script is exactly what this component should not need.
Where anchor positioning exists the bubble is fixed and anchored to the trigger, so a scrolling ancestor cannot clip it, and `data-placement` moves it to any of the four sides. Where it is missing the bubble falls back to absolute positioning above the trigger, which is fine until it sits inside something that scrolls.
A caret bridges the gap between the bubble and the trigger, on whichever side the bubble sits. It is anchored to the trigger rather than to the bubble, because a bubble near the edge of the viewport slides sideways to stay on screen and a caret carried along with it would end up pointing at nothing. It takes the bubble's color and needs no markup of its own.
```html
Copies the link
```
### Accessibility
The trigger references the bubble with `aria-describedby`, so a screen reader announces the hint with the control rather than leaving it stranded in the page. The bubble appears on focus as well as hover, so a keyboard reader gets it too.
Keep it to a few words, and put nothing interactive inside: there is no way to move focus into a bubble that disappears when focus leaves the trigger. If the hint is long, or if it matters, it is not a tooltip. A field's own hint sits under the control permanently and is almost always the better answer.
The bubble also cannot be dismissed while the trigger stays hovered or focused: WCAG 1.4.13 asks for that, and a tooltip with no script has no way to offer it. This is a known limit of a pure-CSS tooltip, not an oversight, and it is another reason to keep the text short and never essential.
Children:
- > *: at least 2, at most 2. The trigger first, then the bubble.
- > [role="tooltip"]: at least 1, at most 1. The bubble; the trigger names it with aria-describedby.
Tokens:
- --yeti-tooltip-surface — Background of the bubble.
- --yeti-tooltip-text — Text of the bubble.
- --yeti-tooltip-radius — Corner of the bubble.
- --yeti-text-sm — The bubble's text size.
- --yeti-space-xs — Block padding of the bubble, its distance from the trigger, and the caret's size.
- --yeti-space-sm — Inline padding of the bubble.
- --yeti-duration-fast — How long the bubble takes to appear.
- --yeti-ease — The curve of that transition.
Accessibility contract:
- Tab: Focusing the trigger shows the bubble, so it is reachable without a mouse.
The trigger must reference the bubble with aria-describedby, so the hint is announced together with the control; the bubble carries role="tooltip". A tooltip is for a short hint on a control whose purpose is not otherwise obvious, and it is never the only place something is said: touch has no hover, so anything essential belongs in the label, in a field's hint, or in the text. Do not put a link or a button inside the bubble, since there is no way to reach one. The bubble cannot be dismissed while the trigger stays hovered or focused, a known limit of a tooltip with no script, which WCAG 1.4.13 asks content shown on hover or focus to offer.
## attention (utility, class="attention")
Plays one pulse or one shake on load, to point at something that has just changed.
Attributes:
- data-attention: pulse (default) | shake — Which gesture: a swell and settle, or a shake from side to side.
### When to use it
On something that has just changed and might otherwise be missed: the alert that appeared after a save, the total that moved when a quantity changed, the field the server rejected. It plays on load, so in practice it arrives with the element, whether that element came from a navigation or was written into the page.
Not as an ornament, and not on anything permanent. A pulse on a heading that has always been there is an instruction to look at something that has nothing to say.
```html
Card declined
```
### How it works
One run of one animation, over `--yeti-attention-duration`. `pulse` swells the element to `--yeti-attention-scale` at the halfway point and settles back; the peak is in the middle so the gesture is symmetrical, since something that grows and snaps back reads as a twitch. `shake` throws it `--yeti-attention-distance` to each side and back twice, because a single sideways move is a slide and it takes the return trip to read as a shake.
It runs once, and the iteration count is deliberately left at the initial `1` rather than reading `--yeti-motion-iterations`, which exists for the animations that never end. A gesture that keeps repeating stops being a moment that has passed and becomes a state the page is in, and the reader has no way to dismiss it.
No keyframe leaves the element anywhere but where it started, so no fill mode is needed and nothing here can strand an element off its mark.
### Accessibility
Motion is not an announcement. A screen reader hears nothing at all here, so a change worth pointing at is a change worth saying: put the element in a live region, or give it `role="status"`, and the gesture becomes the visual half of something that is already spoken.
Under reduced motion `--yeti-attention-duration` collapses and the element simply sits still, which is the whole of the accommodation — there is nothing left to see, and the live region is still heard.
Tokens:
- --yeti-attention-duration — How long the gesture takes; reduced motion collapses it.
- --yeti-attention-distance — How far a shake throws the element to each side.
- --yeti-attention-scale — How large a pulse swells the element at its peak.
- --yeti-ease — The easing of the gesture.
Accessibility contract:
Motion is not an announcement. A change worth pointing at is worth saying, so put the element in a live region, or give it role="status", and the gesture becomes the visual half of something a screen reader already hears. It plays once and never repeats, so nothing here keeps moving under a reader who cannot dismiss it, and under reduced motion the duration collapses and the element simply sits still.
## billboard (utility, class="billboard")
Sizes display text to the container it is in, clamped between two steps of the type scale, so a headline fills the board it is on.
Attributes:
- data-fit: xs-sm | xs-md | xs-lg | xs-xl | xs-2xl | xs-3xl | xs-display | sm-md | sm-lg | sm-xl | sm-2xl | sm-3xl | sm-display | md-lg | md-xl | md-2xl | md-3xl (default) | md-display | lg-xl | lg-2xl | lg-3xl | lg-display | xl-2xl | xl-3xl | xl-display | 2xl-3xl | 2xl-display | 3xl-display — The two ends of the clamp, as a pair of type steps: the smallest the line may set and the largest.
### When to use it
Display text: a hero's headline, a figure's number, a pull quote, the title on a card that is sometimes a column and sometimes a page. A billboard is sized to fill the board it is on and to be read from wherever the reader is standing, and the default pair says so — it runs from `md` to `3xl`, the top of the everyday scale.
Not body copy. Prose has a measure and a comfortable size, and neither of them is a percentage of a column.
### How it works
`font-size` reads `cqi`, one percent of the container's inline size, so the line is sized by the box it is in rather than by the window. The same heading is one size in a card and another across a page, on the same page, which no viewport unit can say. It shrinks as readily as it grows: a billboard in a narrow column comes down to the bottom of its pair, which with the default is body size, not nothing.
`data-fit` is a pair of steps from the type scale, `-`, and the pair is the two ends of a clamp: the line never sets smaller than the first step or larger than the second, whatever the container does. The attribute keeps the name the value has always had — it is the range the text fits within, the way `data-gap` is the gap — while the class names the thing. Any smaller of the eight steps can pair with any larger one, which makes twenty-eight pairs, and `md-3xl` is what you get when the attribute is absent.
Between the ends the size is a proportion of the container, and the proportion is not a number in the stylesheet. It is the pair's own maximum divided by `--yeti-fit-width`, the container width at which a fitted line reaches its ceiling, `32rem` by default. So a pair that ends higher grows faster, every pair arrives at its ceiling in the same box, and a theme moves all twenty-eight by moving one token. With the default pair it works out between six and eleven percent of the container, depending where the fluid scale itself is standing.
```html
Build interfaces that read their own container
```
The container is the nearest ancestor with `container-type: inline-size`. That is what the [container](container.md) layout is for; `grid`, `timeline`, `nav`, `pagination` and `demo` are already size containers, as is a `card` with a leading figure and a `breakout` with a note; anything else needs a `container` above it, or the line measures the viewport. It has to be an ancestor: an element cannot query itself, so putting `billboard` and `container` on the same element measures the box outside it. With no container anywhere above, `cqi` falls back to the small viewport, so a billboard on a bare page grows with the window instead — a lesser effect, not a broken one.
### Accessibility
The clamp is the accessibility story. A size that is only `cqi` has no floor and no ceiling: it shrinks past legibility in a narrow column and runs off the top of a wide screen, and a reader who zooms in gets neither end back. The floor and the ceiling here are steps of the type scale, set in `rem`, so a larger default font size raises the floor and a billboard can never fall below a legible step; the middle of the ramp, between the two ends, follows the container instead.
Nothing here changes what an element is. A billboard `h1` is an `h1`; the class sizes it and says nothing about its rank.
Tokens:
- --yeti-fit-width — The container width at which a fitted line reaches the top of its pair; below it the size is in proportion.
- --yeti-tracking-heading — letter-spacing of the billboard.
- --yeti-text-xs — The xs end of a pair.
- --yeti-text-sm — The sm end of a pair.
- --yeti-text-md — The md end of a pair, and the floor when no pair is given.
- --yeti-text-lg — The lg end of a pair.
- --yeti-text-xl — The xl end of a pair.
- --yeti-text-2xl — The 2xl end of a pair.
- --yeti-text-3xl — The 3xl end of a pair, and the ceiling when no pair is given.
- --yeti-text-display — The display end of a pair, the largest step there is.
Accessibility contract:
The clamp is the accessibility story. A size that is only a container unit has no floor and no ceiling: it shrinks past legibility in a narrow column and runs off the top of a wide screen, and a reader who zooms gets neither end back. The floor and the ceiling here are steps of the type scale, set in rem, so a larger default font size raises the floor and a fitted line can never fall below a legible step; the middle of the ramp, between the two ends, follows the container instead. Use it on a line, not on prose: body copy has a comfortable size that is not a percentage of a column.
## enter (utility, class="enter")
Plays one arrival, on load or as the element scrolls into view, either for the element itself or for each of its children in turn.
Attributes:
- data-enter: fade (default) | rise | fall | slide | scale — Which arrival: a fade, a rise from below, a fall from above, a slide from the side data-side names, or a growth from slightly small.
- data-side: start (default) | end — Which side a slide comes from. The sides are logical, so start is the right edge in a right-to-left page.
- data-stagger (boolean) — Animate the element's children one after another instead of the element itself, each a step later than the last.
- data-view (boolean) — Play as the element scrolls into view instead of on load. Where a scroll timeline is unsupported, or the reader has asked for less motion, it plays on load instead.
- data-once (boolean) — Arrives once, as it first comes near the viewport; without enter.js it is simply there, not animated.
module: enter.js (optional)
### When to use it
Once, near the top of a page, on the thing the page is about: a row of product cards, a hero's headline, the three panels a landing page is built from. An arrival is a punctuation mark, and a page that punctuates every sentence is harder to read than one that punctuates none. If everything on the page enters, nothing has entered.
### How it works
One animation, one run, ending exactly where the layout already put the element. Every keyframe list names only a `from`, so the `to` is the element's own computed style: `enter` has no opinion about where anything belongs and cannot move it. The animation fills backwards, which means its first frame applies from the moment the element is painted, so nothing has to be given `opacity: 0` by a rule of its own. That detail is what makes the whole utility safe to fail: an element whose animation never runs is visible, not stranded.
`data-enter` picks the arrival. `fade` is the default and is the quietest. `rise` starts `--yeti-enter-distance` below its place, which is the one to reach for when the element is a card or a panel arriving into a row. `scale` starts at `--yeti-enter-scale` of its size, for something that should feel like it landed rather than slid.
`fall` is the rise's opposite, from `--yeti-enter-distance` above, for something that drops into place from a bar or a heading. `slide` comes in from the side, the start edge unless `data-side="end"` says otherwise, and the sides are logical: in a right-to-left page the start edge is the right one, and the slide follows without the markup changing.
`data-stagger` moves the animation down one level: the element itself no longer animates and each of its children does, every one `--yeti-enter-stagger` behind the one before it. Put it on the layout that holds the row, not on the items.
```html
One
Two
Three
```
An element on its own can also be timed: set `--yeti-enter-delay` on it and it waits that long before it arrives. That is how a legend's rows land in time with markers drawn on a map, an order a stagger's count cannot express.
Past the ninth child the delay stops growing and every remaining child shares the ninth's. A stagger that kept counting would turn a list of thirty into a ten-second wait, and the gesture is long over by then.
`data-view` ties the arrival to scroll position rather than to time: the animation's progress follows how far the element has crossed into the scrollport, so it plays backwards when the reader scrolls back up and plays again on the way back down. That is a deliberate scroll-linked effect, not a single arrival that merely waits for the viewport — an element with `data-view` announces itself on every crossing, not once. It is guarded twice, and both fallbacks land on the same place: the element plays its arrival on load. Once for `@supports`, because `animation-timeline: view()` is not yet Baseline — Chromium and Safari have it, Firefox does not, and that line will move; and once for `prefers-reduced-motion`, because a scroll-driven animation is paced by the scroll and never reads `animation-duration`, so the collapsed token that stills everything else in Yeti would sail straight past this one.
`data-once` arrives once, as it first comes near the viewport, rather than on load. `enter.css` gives it `animation: none` while the attribute is there, so it simply sits present and unanimated; `enter.js` removes the attribute the moment an `IntersectionObserver` says the element is near, which is the whole mechanism — the rules underneath apply exactly as if `data-once` had never been there, and the animation that starts is a new one, not a resumption of anything, so scrolling away and back never replays it. Without the script `data-once` does nothing useful on its own: `enter.css`'s fallback is to leave the attribute alone and simply not animate the element at all, so it needs `enter.js` to ever arrive. With `data-stagger` the same attribute removal reaches the parent, and each child's own arrival rule applies underneath it exactly as it would for any other `.enter[data-stagger]`.
Because it is enter.js that decides when the arrival starts and enter.css that decides how it plays, `data-once` combines with `data-view` without either needing to know about the other: once the attribute is gone, an element with both simply becomes an ordinary `data-view` element, scroll-linked from that point on.
### Accessibility
Nothing here changes reading order, focus order, or what any element is. The only way an element is ever hidden is the animation's own backwards fill, so a browser that cannot run the animation shows the content instead of swallowing it. `data-once` is safe the same way from the other direction: without `enter.js` the element simply keeps `animation: none` and stays visible, present but never animated, rather than stranded invisible.
Under reduced motion both `--yeti-enter-duration` and `--yeti-enter-stagger` collapse, so every element is simply present at once — the step collapses too, because a stagger with the smooth part removed is still a row of boxes popping into place one after another. The delay collapses as well, even one set on the element itself, so a delayed element is simply present. The scroll-driven form is switched off outright for the same reader, and they get the collapsed load-timed animation instead. `data-once` still waits for the viewport either way; the collapse only means that once it starts, it is instant rather than visibly animated.
Tokens:
- --yeti-enter-ease — The curve the entrance runs on.
- --yeti-enter-delay — How long an element waits before it arrives; for a staggered run, how long before its first child moves.
- --yeti-enter-duration — How long one arrival takes; reduced motion collapses it.
- --yeti-enter-distance — How far below its place a rising element starts.
- --yeti-enter-scale — How small a scaling element starts.
- --yeti-enter-stagger — The wait between one staggered child and the next.
Accessibility contract:
Purely visual, and safe by default: the element is only ever hidden by the animation's own backwards fill, so an element whose animation never runs is visible rather than stranded, and nothing here changes reading order or focus order. Under reduced motion the duration and the stagger step both collapse, so everything is simply present at once; the scroll-driven form is switched off entirely for the same reader, because a scroll timeline paces itself and would ignore a collapsed duration. data-once arrives once, as it first comes near the viewport; without enter.js it is simply there, not animated, since enter.css only withholds the animation while the attribute is present.
## lede (utility, class="lede")
The standfirst: the sentence under a page's heading that says what the page is, set larger than the prose it introduces.
### When to use it
The one sentence under a page's heading that says what the page is, before the page begins. A standfirst. Put it on that paragraph and nothing else: a second lede on a page is not a lede.
### How it works
It is a class rather than a rule about position, because a lede is a role and not a place. The obvious alternative, styling the paragraph that follows the heading, encodes where the paragraph happens to sit, so the day someone wraps the heading and the paragraph in a `stack` the lede quietly becomes body copy. A class survives being moved.
The size is a token, so a theme sets how much larger a lede reads. It carries its own measure, because a lede set larger at the body's measure runs to a longer line than the prose it introduces. Change one and change the other: `ch` is a unit of the font, so raising the size raises what the count is worth.
```html
Card
A bordered surface for one thing.
One thing in a box: an article in a listing, a product, a person, a plan.
```
### Accessibility
Nothing to do. A lede is a paragraph that is set larger; it takes no role and announces nothing of its own, which is correct, because the sentence is ordinary prose and its position already says what it is.
Tokens:
- --yeti-lede-size — How large a lede reads.
- --yeti-lede-measure — The lede's line length; moves with its size, since ch is a unit of the font.
- --yeti-leading-md — Its line height, the same as body prose.
Accessibility contract:
A lede takes no role and announces nothing of its own. It is ordinary prose set larger, and its position under the heading already says what it is.
## lift (utility, class="lift")
Raises the element slightly, or grows it a little, and deepens its shadow while the pointer is on it or a keyboard is inside it.
Attributes:
- data-lift: rise (default) | scale — Which gesture: rise by --yeti-lift-distance, or scale by --yeti-lift-scale, for a tile in a row where a rise would break the line.
### When to use it
On something that can be pressed and is big enough for the movement to read: a card that is a link, a tile in a grid of choices, a panel that opens something. On a button it is noise — a button already has a pressed and a hovered state — and on a paragraph it is a lie, because nothing happens when you click it.
```html
```
### How it works
A transition on `translate` and `box-shadow`, and one rule that raises the element `--yeti-lift-distance` and swaps in `--yeti-lift-shadow`. It answers three selectors: `:hover`, `:focus-visible` on the element itself, and `:has(:focus-visible)`. The third is the one that matters most in practice, because the thing that takes focus is almost always inside — a card's stretched link, a button in a tile — and without it the lift would be an affordance only a pointer ever sees.
`translate` rather than `transform`, so an element that its own component already scales or rotates keeps that.
`data-lift="scale"` grows the element by `--yeti-lift-scale` instead of raising it, with the same deepened shadow. Reach for it on a tile in a row of tiles, where a rise would lift one out of the line; a growth keeps the line and still says "this one". Reduced motion takes the growth to one the way it takes the distance to zero.
### Accessibility
The lift sits on top of a focus ring; it never replaces one. The ring comes from the reset and nothing here touches it.
Reduced motion is answered differently here from everywhere else in Yeti, and on purpose. Everywhere else the animation runs once and ends, so collapsing its duration lands on the end state instantly, which is what was asked for. A hover has no end: its end state is a quarter of a rem up, so a collapsed duration would still move the element, just without the smooth part, and every pass of the pointer would jump the box out from under the cursor and drop it back on the way out. That is more startling than the animation it replaced. So `--yeti-lift-distance` goes to zero instead. The element does not move at all, the deeper shadow carries the whole hover, and the transition that is left has nothing to animate but that shadow.
Tokens:
- --yeti-lift-ease — The curve the lift runs on.
- --yeti-lift-duration — How long the lift takes; match a tinted hover to it.
- --yeti-lift-distance — How far the element rises; reduced motion takes it to zero.
- --yeti-lift-scale — How much the element grows at data-lift="scale"; reduced motion takes it to one.
- --yeti-lift-shadow — The shadow the element casts once lifted.
Accessibility contract:
The lift answers :focus-visible on the element and on anything inside it, so a reader tabbing through a page of cards sees the same affordance a pointer gets. It is decoration on top of a focus ring, never a replacement for one; the ring comes from the reset and is untouched. Under reduced motion the element no longer moves and the deeper shadow carries the hover by itself.
## print (utility, class="print")
Marks an element as belonging to one medium: on the printed page only, or on the screen only.
Attributes:
- data-print: only (default) | none — Which medium the element belongs to: only, on paper and nowhere else, or none, everywhere but paper.
### When to use it
Two jobs, and they are opposites.
Something the paper needs and the screen does not: the address behind a link, since a printed link is a dead end; the date a page was printed; a note about where the document came from.
```html
Printed from https://foundationcss.com/guides/visibility
```
Something the screen needs and the paper does not: a share button, a video, a "back to top" link, a cookie banner, a nav bar. Give those `data-print="none"`.
```html
```
### How it works
Two rules in two media queries. `only`, which is what you get when the attribute is absent, hides the element everywhere but the printed page; `none` hides it on the printed page and nowhere else. Both use `display: none`, and both sit in the utilities layer, which is last in Yeti's cascade order, so a component's own `display` does not win: a `button` really does come off the page.
This is the one place in Yeti where a media query is the right tool. Every width in the framework is a container query, because a rule that switches on the window breaks the moment its element moves into a narrower box. A medium is not a width — no container is ever printed at a different medium from the page around it — so there is no box to measure here and nothing a container query could say.
Print styling stops there. Yeti does not set page margins, force a color scheme, or break pages for you; `@page`, `break-inside` and the rest are yours to write, and this utility is only about which elements are on the sheet at all.
### Accessibility
Both states are `display: none` in the medium they are not for, which takes the element out of the accessibility tree there too. That is the right behaviour for this job, and it has two consequences worth stating.
A paper-only line is not announced on screen. Never put anything a screen reader needs into one — an address a sighted reader can also see is fine, a warning is not. Text that should be heard and not seen is [visually-hidden](visually-hidden.md), which is a different tool for a different question; the [visibility guide](guides/visibility.md) has both side by side.
A screen-only control is not on the page a reader prints. Nothing on paper may depend on it, so a form whose only submit button carries `data-print="none"` has printed a form nobody can return.
`print` and `visually-hidden` do not combine on one element: on screen `print` removes it outright, so there is nothing left for `visually-hidden` to keep announced, and on paper the clip `visually-hidden` applies leaves a blank pixel where the printed content should be.
Accessibility contract:
Both states are display: none in the medium they are not for, which takes the element out of the accessibility tree there as well. That is correct for this job and worth saying out loud: a paper-only line is not read out on screen, so it must never carry anything a reader on screen needs, and a screen-only control is not on the page a reader prints, so nothing on paper may depend on it. Text a screen reader should hear but nobody should see is visually-hidden, not this. A printed page is also a page with no color guarantee, so never let the print state be the only thing carrying a meaning.
## visually-hidden (utility, class="visually-hidden")
Takes words out of sight and leaves them in the accessibility tree, for the name a control needs when its face is a picture.
### When to use it
Words a screen reader needs and the page has no room for: the name of an icon-only button, the number on a carousel dot, the subject of the third "Read more" link on a page.
It is not the `hidden` attribute, and the difference is the whole point — it is also why this is the one class in Yeti with two words in its name. `hidden` takes the element out of the page and out of the accessibility tree with it, so a reader hears nothing. This class takes it out of sight and leaves it in the tree, so a reader hears everything. The [visibility guide](guides/visibility.md) has both, and the ten other ways to hide something, on one page.
```html
Read more about the trail map
```
### How it works
The old clip-rect recipe, with `clip-path` in place of the deprecated `clip` property. The box is taken out of the flow with `position: absolute`, so it cannot open a gap in the line it sits in; it is sized a pixel square, because some engines drop a zero-size box altogether; and it is clipped to nothing with `clip-path: inset(50%)` and `overflow: hidden`. `white-space: nowrap` stops a long string being wrapped into a one-character column, which some screen readers read out letter by letter.
`display: none` and `visibility: hidden` are both shorter, and both do the one thing this must not: they take the text out of the accessibility tree.
Put only text in here. Everything inside is still in the page, so a link or a button inside would take Tab focus with nothing on screen to show for it, which is worse than either showing it or leaving it out.
### Accessibility
The text is announced in source order and becomes part of the accessible name of whatever contains it, which is what makes a button with a picture on its face and a word inside it a named button.
A control is named once. Where an `aria-label` is present the browser uses that and ignores the text inside, so pick one: the label when the name is a rewording of the content, the hidden text when the name *is* the content and the design has no room for it.
It is a tool of last resort. A control with room for a word should have the word.
Accessibility contract:
This is the opposite of the hidden attribute, which removes the element from the accessibility tree as well: text carrying this class is still announced, in source order, and becomes part of the accessible name of whatever contains it. The two words in the name are there to keep the two apart. A control named twice is named once: where an aria-label is present the browser uses it and ignores the text inside, so use one or the other. Put only text in here. Anything focusable is still focusable and would take Tab focus with nothing on screen to show for it.
## Tokens
Every public custom property, with its group and default. A theme is a file that sets some of these after yeti.css.
- --yeti-base (scale): unset — Set to pin the body size at one value for every viewport; overrides both --yeti-base-min and --yeti-base-max.
- --yeti-ratio (scale): unset — Set to pin the scale ratio at one value for every viewport; overrides both --yeti-ratio-min and --yeti-ratio-max.
- --yeti-base-min (scale): 1rem — Body text size at the narrow viewport.
- --yeti-base-max (scale): 1.125rem — Body text size at the wide viewport.
- --yeti-ratio-min (scale): 1.2 — Scale ratio at the narrow viewport (minor third).
- --yeti-ratio-max (scale): 1.333 — Scale ratio at the wide viewport (perfect fourth).
- --yeti-viewport-min (scale): 320px — Viewport width at which the narrow values apply.
- --yeti-viewport-max (scale): 1280px — Viewport width at which the wide values apply.
- --yeti-space-none (space): 0 — No space; the value behind data-gap="none".
- --yeti-space-xs (space): step -2 — Hairline gap.
- --yeti-space-sm (space): step -1 — Tight gap.
- --yeti-space-md (space): step 0 — Default gap; equals the body text size.
- --yeti-space-lg (space): step 1 — Comfortable gap.
- --yeti-space-ratio (space): 2 — How much each space stop above lg grows on the one before; xl is base × ratio, 4xl is base × ratio⁴. Keep it above --yeti-ratio-max so xl stays larger than lg.
- --yeti-space-xl (space): base × space-ratio — Section gap; twice the body size.
- --yeti-space-2xl (space): base × space-ratio² — Page-level gap; four bodies.
- --yeti-space-3xl (space): base × space-ratio³ — Hero-level gap; eight bodies.
- --yeti-space-4xl (space): base × space-ratio⁴ — The biggest room: sixteen bodies, the gap between a page's great sections.
- --yeti-space-xs-static (space): step -2, narrow viewport value — Non-fluid xs, for a gap that must not scale.
- --yeti-space-sm-static (space): step -1, narrow viewport value — Non-fluid sm.
- --yeti-space-md-static (space): step 0, narrow viewport value — Non-fluid md.
- --yeti-space-lg-static (space): step 1, narrow viewport value — Non-fluid lg.
- --yeti-space-xl-static (space): narrow base × space-ratio — Non-fluid xl.
- --yeti-space-2xl-static (space): narrow base × space-ratio² — Non-fluid 2xl.
- --yeti-space-3xl-static (space): narrow base × space-ratio³ — Non-fluid 3xl.
- --yeti-space-4xl-static (space): narrow base × space-ratio⁴ — Non-fluid 4xl.
- --yeti-radius-sm (radius): 0.25 × base — Small corner radius; inputs, code, badges.
- --yeti-radius-md (radius): 0.5 × base — Medium corner radius; cards, dialogs.
- --yeti-radius-lg (radius): 1 × base — Large corner radius; hero panels.
- --yeti-radius-full (radius): 9999px — Pill or circle.
- --yeti-width-3xs (width): 8rem — The narrowest named width: two of them fit a phone, so a grid of links, badges or small pictures at data-min="3xs" is two abreast where 2xs gives one.
- --yeti-width-2xs (width): 12rem — The narrowest named width: three of them fit a reading column, which sixteen rem does not.
- --yeti-width-xs (width): 16rem — Smallest container measure; the grid's default minimum column width.
- --yeti-width-sm (width): 24rem — The sidebar's default width.
- --yeti-width-md (width): 32rem — The columns layout's default threshold.
- --yeti-width-lg (width): 48rem — A narrow page column.
- --yeti-width-xl (width): 64rem — The center layout's default maximum.
- --yeti-width-2xl (width): 80rem — A wide page column.
- --yeti-height-sm (height): 12rem — Block size at data-height="sm"; the demo's preview box is the first taker.
- --yeti-height-md (height): 20rem — Block size at data-height="md", and with no attribute.
- --yeti-height-lg (height): 32rem — Block size at data-height="lg".
- --yeti-height-xl (height): 48rem — Block size at data-height="xl".
- --yeti-height-half (height): 50dvh — Block size at data-height="half": half the viewport, dynamic so it tracks the browser chrome on phones.
- --yeti-text-xs (text): step -2 — Captions and fine print.
- --yeti-text-sm (text): step -1 — Small text; h6.
- --yeti-text-md (text): step 0 — Body text; h5 in bold.
- --yeti-text-lg (text): step 1 — Lead paragraphs; h4.
- --yeti-text-xl (text): step 2 — h3.
- --yeti-text-2xl (text): step 3 — h2.
- --yeti-text-3xl (text): step 4 — h1.
- --yeti-text-display (text): step 5 — Display type, larger than any heading.
- --yeti-leading-md (text): 1em + 0.5 × base — Body line height; about 1.5 at body size, tightening as type grows.
- --yeti-leading-sm (text): 1.5 — Line height for text set below the body size, such as a code block; a ratio, so it shrinks with the type.
- --yeti-leading-tight (text): 1.15 — Heading line height.
- --yeti-measure (text): 70ch — Maximum line length for prose.
- --yeti-list-gap (text): 0.25rem — Space between one item of a prose list and the next.
- --yeti-heading-space-before (text): var(--yeti-space-xl) — Space above a section heading (h2, h3) in flow, from what comes before it. Read by base flow and by breakout.
- --yeti-heading-space-after (text): var(--yeti-space-sm) — Space between any heading in flow and what it opens. Read by base flow and by breakout.
- --yeti-lede-size (text): var(--yeti-text-lg) — How large a lede reads.
- --yeti-lede-measure (text): 50ch — A lede's line length. Moves with --yeti-lede-size, since ch is a unit of the font.
- --yeti-font-sans (font): system-ui, sans-serif — Default text face. Yeti ships no web fonts; override this to use yours.
- --yeti-font-mono (font): ui-monospace, monospace — Code face.
- --yeti-font-heading (font): var(--yeti-font-sans) — Heading face, every level. The text face until a theme sets a second family; one token instead of six selector overrides.
- --yeti-stretch-text (font): normal — font-stretch of the body: the width axis of a variable font, as a keyword or a percentage.
- --yeti-stretch-heading (font): normal — font-stretch of the headings, set apart from the text so a theme can pair narrow text with a wide title in one family.
- --yeti-stretch-small (font): var(--yeti-stretch-text) — font-stretch of small lettering: captions, small print, a nav's links, a badge. The text width unless a theme narrows it.
- --yeti-tracking-heading (text): normal — letter-spacing of every heading level and the billboard. Set in em so a large heading tightens more than a small one; a page may set it on one level.
- --yeti-duration-fast (motion): 150ms — Quick transitions; hover and focus. Collapses to 0.01ms under prefers-reduced-motion.
- --yeti-duration-base (motion): 250ms — Standard transitions; open and close. Collapses under prefers-reduced-motion.
- --yeti-ease (motion): cubic-bezier(0.2, 0, 0, 1) — Default easing; fast out, gentle in.
- --yeti-motion-iterations (motion): infinite — Iteration count for animations that run forever, such as the spinner. Collapses to 1 under prefers-reduced-motion, because a component's own infinite would outrank the reset and keep running at a random phase every frame.
- --yeti-page-duration (motion): 200ms — How long the crossfade between two pages takes, for a site that has turned cross-document view transitions on. Collapses to 0.01ms under prefers-reduced-motion, which is the only thing that stills a page transition.
- --yeti-page-ease (motion): ease — The curve that crossfade runs on. Symmetric, unlike --yeti-ease, because neither end of a page change leads.
- --yeti-hue-primary (hue): 250 — Brand hue in oklch degrees; 250 is blue. Every primary color derives from it.
- --yeti-hue-secondary (hue): 300 — Secondary hue; 300 is violet.
- --yeti-hue-success (hue): 145 — Success hue; 145 is green.
- --yeti-hue-warning (hue): 80 — Warning hue; 80 is amber.
- --yeti-hue-alert (hue): 25 — Alert hue; 25 is red.
- --yeti-hue-neutral (hue): var(--yeti-hue-primary) — Hue that faintly tints surfaces, text, and borders. Follows the primary hue unless set.
- --yeti-chroma (hue): 0.15 — Accent saturation. Lower for a calmer palette; 0.2 and above may clip on very saturated hues.
- --yeti-color-primary (color): ladder base of the primary hue — The brand color: buttons, links, focus.
- --yeti-color-primary-subtle (color): ladder subtle — Tinted background; hover on surfaces, selection.
- --yeti-color-primary-soft (color): ladder soft — Borders and dividers in the primary hue.
- --yeti-color-primary-strong (color): ladder strong — Hover and active state of the primary color.
- --yeti-color-primary-text (color): ladder text — Primary-hued text on plain surfaces.
- --yeti-on-primary (color): near-white in light, near-black in dark — Text placed on --yeti-color-primary.
- --yeti-color-secondary (color): ladder base of the secondary hue — Secondary accent.
- --yeti-color-secondary-subtle (color): ladder subtle — Tinted background in the secondary hue.
- --yeti-color-secondary-soft (color): ladder soft — Borders in the secondary hue.
- --yeti-color-secondary-strong (color): ladder strong — Hover and active state of the secondary color.
- --yeti-color-secondary-text (color): ladder text — Secondary-hued text on plain surfaces.
- --yeti-on-secondary (color): near-white in light, near-black in dark — Text placed on --yeti-color-secondary.
- --yeti-color-success (color): ladder base of the success hue — Positive state.
- --yeti-color-success-subtle (color): ladder subtle — Tinted background for success messages.
- --yeti-color-success-soft (color): ladder soft — Borders for success messages.
- --yeti-color-success-strong (color): ladder strong — Hover and active state of the success color.
- --yeti-color-success-text (color): ladder text — Success-hued text on plain surfaces.
- --yeti-on-success (color): near-white in light, near-black in dark — Text placed on --yeti-color-success.
- --yeti-color-warning (color): ladder base of the warning hue — Caution state.
- --yeti-color-warning-subtle (color): ladder subtle — Tinted background for warnings.
- --yeti-color-warning-soft (color): ladder soft — Borders for warnings.
- --yeti-color-warning-strong (color): ladder strong — Hover and active state of the warning color.
- --yeti-color-warning-text (color): ladder text — Warning-hued text on plain surfaces.
- --yeti-on-warning (color): near-white in light, near-black in dark — Text placed on --yeti-color-warning.
- --yeti-color-alert (color): ladder base of the alert hue — Error and danger state.
- --yeti-color-alert-subtle (color): ladder subtle — Tinted background for errors.
- --yeti-color-alert-soft (color): ladder soft — Borders for errors.
- --yeti-color-alert-strong (color): ladder strong — Hover and active state of the alert color.
- --yeti-color-alert-text (color): ladder text — Alert-hued text on plain surfaces.
- --yeti-on-alert (color): near-white in light, near-black in dark — Text placed on --yeti-color-alert.
- --yeti-color-neutral-subtle (color): ladder subtle — Neutral tint for badges and quiet fills.
- --yeti-color-neutral-soft (color): ladder soft — Neutral fill for hover states.
- --yeti-color-neutral (color): ladder base — Neutral accent: buttons and badges with data-variant="neutral".
- --yeti-color-neutral-strong (color): ladder strong — Neutral accent, one step darker: hover and pressed.
- --yeti-color-neutral-text (color): ladder text — Neutral-hued text on plain surfaces.
- --yeti-on-neutral (color): near-white in light, near-black in dark — Text placed on --yeti-color-neutral.
- --yeti-color-surface (color): L 0.99 light, 0.13 dark — Page background.
- --yeti-color-surface-raised (color): L 0.97 light, 0.17 dark — Cards, code blocks, anything sitting on the page.
- --yeti-color-surface-sunken (color): L 0.94 light, 0.10 dark — Wells and inset areas.
- --yeti-color-text (color): L 0.15 light, 0.93 dark — Body text.
- --yeti-color-text-muted (color): L 0.45 light, 0.70 dark — Secondary text: captions, help, metadata.
- --yeti-color-border (color): L 0.88 light, 0.30 dark — Default border and divider.
- --yeti-color-border-strong (color): L 0.75 light, 0.40 dark — Emphasised border: focused inputs, blockquote rule.
- --yeti-color-scrim (color): the surface at 85% — A wash over content so something on top of it can be read: a caption over a picture, a veil over a form. Thinned surface, so it dims with the theme rather than tinting.
- --yeti-color-focus (color): var(--yeti-color-primary) — Focus ring color.
- --yeti-link-color (color): var(--yeti-color-primary) — Text color of a link at rest. Read with a fallback, so a section's own primary still colors its links. A theme whose primary is too light to read as text points this at the hue's text step, --yeti-color-primary-text.
- --yeti-link-color-hover (color): var(--yeti-color-primary-strong) — Text color of a link under the pointer. Read with a fallback, so a section's own primary still colors its links.
- --yeti-cover-height (layout): 100dvh — Minimum block size of a cover layout or a filled stack. Set to auto for one that is only as tall as its content.
- --yeti-sticky-offset (layout): var(--yeti-space-md) — How far from the leading edge of the scrollport a child carrying data-sticky stops. Set it to a sticky bar's height on whatever holds the things that stick below it.
- --yeti-scroll-padding (layout): var(--yeti-sticky-offset) — Where a scroll-into-view stops below the top edge: a fragment jump, scrollIntoView, a focused control. The sticky offset by default; set it alone for more room above a jumped-to heading than the things that stick need. Root-level: it is read by the root's scroll padding.
- --yeti-border-width (border): 1px — Width of every border a component draws.
- --yeti-weight-normal (weight): 400 — Body weight.
- --yeti-weight-bold (weight): 700 — th, strong, b and, through --yeti-weight-heading, the headings. Separate from --yeti-weight-strong so display text and control labels can be tuned apart.
- --yeti-weight-heading (weight): var(--yeti-weight-bold) — The headings' own weight, bold unless a theme sets it: a display face often wants 500 or 800 where strong text stays at 700.
- --yeti-weight-strong (weight): 600 — Labels, headers, buttons.
- --yeti-shadow-color (shadow): black at 14% in light, 55% in dark — The color every shadow is drawn in; a theme changes offsets without losing the scheme flip.
- --yeti-shadow-sm (shadow): 0 1px 2px (black at 14% light, 55% dark) — Raised cards.
- --yeti-shadow-md (shadow): 0 4px 12px (the same color) — Floating surfaces; phase 4's menus and dialogs.
- --yeti-control-size (control): 2.5rem — Minimum block size of a button or input at data-size="md".
- --yeti-control-radius (control): var(--yeti-radius-sm) — Corner of inputs.
- --yeti-control-border (control): var(--yeti-color-border) — Border of inputs at rest.
- --yeti-control-surface (control): var(--yeti-color-surface) — Background of inputs.
- --yeti-control-chevron (control): a mid-grey chevron — The select's chevron image; a theme may supply its own, for example a lighter one for a dark control surface.
- --yeti-opacity-muted (control): 0.6 — Opacity of a disabled or busy control.
- --yeti-button-radius (button): var(--yeti-radius-md) — Corner of buttons.
- --yeti-button-weight (button): var(--yeti-weight-strong) — Weight of button text.
- --yeti-button-padding (button): 2 — Inline padding as a multiple of the size's space step.
- --yeti-button-padding-block (button): 0.25 — Block padding as a multiple of the size's space step. The control height still sets the minimum, so this shows on a wrapped label, and a theme raises it for roomier buttons.
- --yeti-badge-radius (badge): var(--yeti-radius-full) — Corner of badges; full makes a pill.
- --yeti-badge-weight (badge): var(--yeti-weight-strong) — Weight of badge text.
- --yeti-card-radius (card): var(--yeti-radius-md) — Corner of cards.
- --yeti-card-padding (card): var(--yeti-space-md) — Padding inside a card.
- --yeti-card-gap (card): var(--yeti-space-sm) — Space between a card's parts.
- --yeti-card-border (card): var(--yeti-color-border) — Border of cards; transparent when raised.
- --yeti-card-surface (card): var(--yeti-color-surface-raised) — Background of cards.
- --yeti-field-gap (field): var(--yeti-space-xs) — Space between a field's label, control, and hint.
- --yeti-switch-track (field): var(--yeti-control-border) — Fill and border of an off switch's track. Keep it 3:1 against the page surface.
- --yeti-switch-thumb (field): var(--yeti-control-surface) — The thumb of an off switch.
- --yeti-table-stripe (table): var(--yeti-color-surface-sunken) — Fill of even rows with data-striped.
- --yeti-table-border (table): var(--yeti-color-border) — Row and cell borders.
- --yeti-seam-size (seam): twice the size's space step — Depth of the cut; set it to fix the depth for every size.
- --yeti-nav-padding (nav): var(--yeti-space-sm) var(--yeti-space-md) — Padding of the bar and of the open panel.
- --yeti-nav-radius (nav): var(--yeti-radius-sm) — Corner of the links and the toggle.
- --yeti-nav-border (nav): var(--yeti-color-border) — The bar's bottom edge, and the sheet's.
- --yeti-nav-surface (nav): var(--yeti-color-surface) — Background of the bar.
- --yeti-nav-panel (nav): var(--yeti-color-surface-raised) — Background of the open panel.
- --yeti-nav-brand-weight (nav): var(--yeti-weight-strong) — Weight of the nav's brand.
- --yeti-nav-brand-size (nav): 1em — Size of the nav's brand; 1em is the bar's own size.
- --yeti-nav-link (nav): currentColor — Color of a plain link in the bar; the bar's own color unless set. The current link and a hovered link keep the variant's tints, and the open panel resets it to its own text color.
- --yeti-breadcrumbs-separator (breadcrumbs): "/" — The string between crumbs; a theme may prefer "›".
- --yeti-pagination-radius (pagination): var(--yeti-radius-md) — Corner of each page target.
- --yeti-alert-radius (alert): var(--yeti-radius-md) — Corner of an alert.
- --yeti-alert-padding (alert): var(--yeti-space-md) — Padding inside an alert.
- --yeti-alert-edge (alert): four times the local border width — Width of an alert's start edge, at every emphasis. Read with a fallback to four times --yeti-border-width, evaluated locally, so a section with its own border width still gets four of its own; set this directly to fix the width regardless of the local border.
- --yeti-progress-radius (progress): var(--yeti-radius-full) — Corner of the bar and of its value.
- --yeti-progress-size (progress): half the size's space step — Thickness of the bar, a length. Read with a fallback to half the size's space step; a hairline reading bar sets 2px.
- --yeti-spinner-duration (spinner): 0.8s — One turn of the ring; reduced motion stops it.
- --yeti-spinner-width (spinner): 0.15em — Thickness of the ring.
- --yeti-accordion-border (accordion): var(--yeti-color-border) — The line between one panel and the next, and around the set.
- --yeti-accordion-padding (accordion): var(--yeti-space-md) — Padding of a summary and of its panel.
- --yeti-accordion-surface (accordion): var(--yeti-color-surface) — Background of the set, and so of an open panel.
- --yeti-accordion-summary (accordion): color-mix(in oklch, var(--yeti-accordion-surface) 88%, var(--yeti-color-text)) — Background of a summary row, set apart from the panel below it. Mixed toward the text color, so it darkens the row in light and lifts it in dark.
- --yeti-accordion-summary-hover (accordion): color-mix(in oklch, var(--yeti-accordion-surface) 80%, var(--yeti-color-text)) — Background of a summary under the pointer, one step further from the panel than at rest.
- --yeti-tabs-border (tabs): var(--yeti-color-border) — The line under the tab list.
- --yeti-tabs-padding (tabs): var(--yeti-space-sm) var(--yeti-space-md) — Padding of one tab.
- --yeti-dropdown-surface (dropdown): var(--yeti-color-surface-raised) — Background of the open panel.
- --yeti-dropdown-radius (dropdown): var(--yeti-radius-md) — Corner of the panel.
- --yeti-dropdown-padding (dropdown): var(--yeti-space-xs) — Padding around the items.
- --yeti-dropdown-min (dropdown): 12rem — Least width of the panel.
- --yeti-dropdown-open-delay (dropdown): 100ms — How long the pointer must rest on a data-trigger="hover" dropdown before its panel opens. Read by hover.js, so it has no effect without that module.
- --yeti-dropdown-close-delay (dropdown): 200ms — How long a hover-triggered panel waits after the pointer leaves before it closes, so the gap between trigger and panel is forgiving. Read by hover.js.
- --yeti-dialog-surface (dialog): var(--yeti-color-surface-raised) — Background of the dialog.
- --yeti-dialog-radius (dialog): var(--yeti-radius-lg) — Corner of the dialog.
- --yeti-dialog-padding (dialog): var(--yeti-space-lg) — Padding inside the dialog.
- --yeti-dialog-backdrop (dialog): black at 60% — The backdrop behind an open dialog. Black rather than a theme color, because a backdrop dims what is behind it in every theme.
- --yeti-tooltip-surface (tooltip): var(--yeti-color-text) — Background of the bubble, inverted so it reads over anything.
- --yeti-tooltip-text (tooltip): var(--yeti-color-surface) — Text of the bubble.
- --yeti-tooltip-radius (tooltip): var(--yeti-radius-sm) — Corner of the bubble.
- --yeti-carousel-dot (carousel): var(--yeti-color-border-strong) — Color of a slide dot.
- --yeti-carousel-dot-size (carousel): 0.75rem — Diameter of a slide dot.
- --yeti-carousel-scroll (carousel): smooth, and auto under reduced motion — How the track scrolls when a dot is followed. A components-layer declaration outranks the reset, so the preference reaches the track through this token.
- --yeti-toc-scroll (toc): smooth, and auto under reduced motion — How the page scrolls when a link in a toc, or an in-page link in a nav, is followed. A components-layer declaration outranks the reset, so the preference reaches the page through this token. The name keeps the toc it started with.
- --yeti-toc-padding (toc): var(--yeti-space-xs) — The rows' block padding: a control's density by default, a book's contents page sets it smaller.
- --yeti-toc-hover (toc): var(--_yeti-variant-subtle) — Background of a toc link under the pointer. Read with a fallback to the variant's subtle tint; set transparent for an underline-only list.
- --yeti-range-value (field): 0% — The filled share of a range's track. Set it on the input, inline or from your own script, since CSS cannot read the value. Not a theme value.
- --yeti-demo-border (demo): var(--yeti-color-border) — The preview box's edge.
- --yeti-demo-radius (demo): var(--yeti-radius-md) — The preview box's corner, the summary's, and the stop label's.
- --yeti-demo-label (demo): var(--yeti-color-surface-sunken) — Surface of the stop label naming the current width, and of the summary.
- --yeti-enter-duration (enter): 600ms — How long one arrival takes. Collapses to 0.01ms under prefers-reduced-motion, which leaves the element present and still.
- --yeti-enter-distance (enter): 2rem — How far below its place a rising element starts.
- --yeti-enter-scale (enter): 0.94 — How small a scaling element starts.
- --yeti-enter-stagger (enter): 200ms — The wait between one staggered child and the next; every child's delay is a multiple of it. Collapses to 0s under prefers-reduced-motion.
- --yeti-enter-delay (enter): 0s — How long an element waits before it arrives, or a staggered run before its first child moves. Set it on one element to time that element alone.
- --yeti-enter-ease (enter): var(--yeti-ease) — The curve a staggered entrance runs on.
- --yeti-lift-distance (lift): 0.25rem — How far the element rises under the pointer. Collapses to 0 under prefers-reduced-motion, so the hover keeps its deeper shadow and loses only the movement.
- --yeti-lift-scale (lift): 1.02 — How much the element grows at data-lift="scale"; reduced motion takes it to one.
- --yeti-lift-shadow (lift): var(--yeti-shadow-md) — The shadow the element casts once lifted.
- --yeti-lift-duration (lift): var(--yeti-duration-fast) — How long the lift takes; a consumer tinting the same hover matches it to this.
- --yeti-lift-ease (lift): var(--yeti-ease) — The curve a lift runs on.
- --yeti-attention-duration (attention): 600ms — How long the one pulse or shake takes. Collapses to 0.01ms under prefers-reduced-motion.
- --yeti-attention-distance (attention): 0.35rem — How far a shake throws the element to each side.
- --yeti-attention-scale (attention): 1.06 — How large a pulse swells the element at its peak.
- --yeti-fit-width (fit): var(--yeti-width-md) — The container width at which a line carrying billboard reaches the top of its pair; below it the size is in proportion, above it the clamp holds.
- --yeti-white (color): oklch(1 0 0) — White, in both schemes and under every theme. One of the three colors that never move.
- --yeti-black (color): oklch(0 0 0) — Black, in both schemes and under every theme.
- --yeti-grey (color): oklch(0.565 0 0) — 18% reflectance, the photographic middle grey, in both schemes and under every theme.
- --yeti-grey-0 (color): 0% from surface to text — The page surface, as the first step of the greyscale.
- --yeti-grey-10 (color): 10% from surface to text — 10% of the way from the page surface to its text. Near the page in both schemes when the number is low.
- --yeti-grey-20 (color): 20% from surface to text — 20% of the way from the page surface to its text. Near the page in both schemes when the number is low.
- --yeti-grey-30 (color): 30% from surface to text — 30% of the way from the page surface to its text. Near the page in both schemes when the number is low.
- --yeti-grey-40 (color): 40% from surface to text — 40% of the way from the page surface to its text. Near the page in both schemes when the number is low.
- --yeti-grey-50 (color): 50% from surface to text — 50% of the way from the page surface to its text. Near the text in both schemes when the number is high.
- --yeti-grey-60 (color): 60% from surface to text — 60% of the way from the page surface to its text. Near the text in both schemes when the number is high.
- --yeti-grey-70 (color): 70% from surface to text — 70% of the way from the page surface to its text. Near the text in both schemes when the number is high.
- --yeti-grey-80 (color): 80% from surface to text — 80% of the way from the page surface to its text. Near the text in both schemes when the number is high.
- --yeti-grey-90 (color): 90% from surface to text — 90% of the way from the page surface to its text. Near the text in both schemes when the number is high.
- --yeti-grey-100 (color): 100% from surface to text — The page text color, as the last step of the greyscale.
- --yeti-color-primary-0 (color): grey-0 at the primary hue — The primary hue at the lightness of --yeti-grey-0; flips with the scheme as the grey does.
- --yeti-color-primary-10 (color): grey-10 at the primary hue — The primary hue at the lightness of --yeti-grey-10; flips with the scheme as the grey does.
- --yeti-color-primary-20 (color): grey-20 at the primary hue — The primary hue at the lightness of --yeti-grey-20; flips with the scheme as the grey does.
- --yeti-color-primary-30 (color): grey-30 at the primary hue — The primary hue at the lightness of --yeti-grey-30; flips with the scheme as the grey does.
- --yeti-color-primary-40 (color): grey-40 at the primary hue — The primary hue at the lightness of --yeti-grey-40; flips with the scheme as the grey does.
- --yeti-color-primary-50 (color): grey-50 at the primary hue — The primary hue at the lightness of --yeti-grey-50; flips with the scheme as the grey does.
- --yeti-color-primary-60 (color): grey-60 at the primary hue — The primary hue at the lightness of --yeti-grey-60; flips with the scheme as the grey does.
- --yeti-color-primary-70 (color): grey-70 at the primary hue — The primary hue at the lightness of --yeti-grey-70; flips with the scheme as the grey does.
- --yeti-color-primary-80 (color): grey-80 at the primary hue — The primary hue at the lightness of --yeti-grey-80; flips with the scheme as the grey does.
- --yeti-color-primary-90 (color): grey-90 at the primary hue — The primary hue at the lightness of --yeti-grey-90; flips with the scheme as the grey does.
- --yeti-color-primary-100 (color): grey-100 at the primary hue — The primary hue at the lightness of --yeti-grey-100; flips with the scheme as the grey does.
- --yeti-color-secondary-0 (color): grey-0 at the secondary hue — The secondary hue at the lightness of --yeti-grey-0; flips with the scheme as the grey does.
- --yeti-color-secondary-10 (color): grey-10 at the secondary hue — The secondary hue at the lightness of --yeti-grey-10; flips with the scheme as the grey does.
- --yeti-color-secondary-20 (color): grey-20 at the secondary hue — The secondary hue at the lightness of --yeti-grey-20; flips with the scheme as the grey does.
- --yeti-color-secondary-30 (color): grey-30 at the secondary hue — The secondary hue at the lightness of --yeti-grey-30; flips with the scheme as the grey does.
- --yeti-color-secondary-40 (color): grey-40 at the secondary hue — The secondary hue at the lightness of --yeti-grey-40; flips with the scheme as the grey does.
- --yeti-color-secondary-50 (color): grey-50 at the secondary hue — The secondary hue at the lightness of --yeti-grey-50; flips with the scheme as the grey does.
- --yeti-color-secondary-60 (color): grey-60 at the secondary hue — The secondary hue at the lightness of --yeti-grey-60; flips with the scheme as the grey does.
- --yeti-color-secondary-70 (color): grey-70 at the secondary hue — The secondary hue at the lightness of --yeti-grey-70; flips with the scheme as the grey does.
- --yeti-color-secondary-80 (color): grey-80 at the secondary hue — The secondary hue at the lightness of --yeti-grey-80; flips with the scheme as the grey does.
- --yeti-color-secondary-90 (color): grey-90 at the secondary hue — The secondary hue at the lightness of --yeti-grey-90; flips with the scheme as the grey does.
- --yeti-color-secondary-100 (color): grey-100 at the secondary hue — The secondary hue at the lightness of --yeti-grey-100; flips with the scheme as the grey does.
- --yeti-color-success-0 (color): grey-0 at the success hue — The success hue at the lightness of --yeti-grey-0; flips with the scheme as the grey does.
- --yeti-color-success-10 (color): grey-10 at the success hue — The success hue at the lightness of --yeti-grey-10; flips with the scheme as the grey does.
- --yeti-color-success-20 (color): grey-20 at the success hue — The success hue at the lightness of --yeti-grey-20; flips with the scheme as the grey does.
- --yeti-color-success-30 (color): grey-30 at the success hue — The success hue at the lightness of --yeti-grey-30; flips with the scheme as the grey does.
- --yeti-color-success-40 (color): grey-40 at the success hue — The success hue at the lightness of --yeti-grey-40; flips with the scheme as the grey does.
- --yeti-color-success-50 (color): grey-50 at the success hue — The success hue at the lightness of --yeti-grey-50; flips with the scheme as the grey does.
- --yeti-color-success-60 (color): grey-60 at the success hue — The success hue at the lightness of --yeti-grey-60; flips with the scheme as the grey does.
- --yeti-color-success-70 (color): grey-70 at the success hue — The success hue at the lightness of --yeti-grey-70; flips with the scheme as the grey does.
- --yeti-color-success-80 (color): grey-80 at the success hue — The success hue at the lightness of --yeti-grey-80; flips with the scheme as the grey does.
- --yeti-color-success-90 (color): grey-90 at the success hue — The success hue at the lightness of --yeti-grey-90; flips with the scheme as the grey does.
- --yeti-color-success-100 (color): grey-100 at the success hue — The success hue at the lightness of --yeti-grey-100; flips with the scheme as the grey does.
- --yeti-color-warning-0 (color): grey-0 at the warning hue — The warning hue at the lightness of --yeti-grey-0; flips with the scheme as the grey does.
- --yeti-color-warning-10 (color): grey-10 at the warning hue — The warning hue at the lightness of --yeti-grey-10; flips with the scheme as the grey does.
- --yeti-color-warning-20 (color): grey-20 at the warning hue — The warning hue at the lightness of --yeti-grey-20; flips with the scheme as the grey does.
- --yeti-color-warning-30 (color): grey-30 at the warning hue — The warning hue at the lightness of --yeti-grey-30; flips with the scheme as the grey does.
- --yeti-color-warning-40 (color): grey-40 at the warning hue — The warning hue at the lightness of --yeti-grey-40; flips with the scheme as the grey does.
- --yeti-color-warning-50 (color): grey-50 at the warning hue — The warning hue at the lightness of --yeti-grey-50; flips with the scheme as the grey does.
- --yeti-color-warning-60 (color): grey-60 at the warning hue — The warning hue at the lightness of --yeti-grey-60; flips with the scheme as the grey does.
- --yeti-color-warning-70 (color): grey-70 at the warning hue — The warning hue at the lightness of --yeti-grey-70; flips with the scheme as the grey does.
- --yeti-color-warning-80 (color): grey-80 at the warning hue — The warning hue at the lightness of --yeti-grey-80; flips with the scheme as the grey does.
- --yeti-color-warning-90 (color): grey-90 at the warning hue — The warning hue at the lightness of --yeti-grey-90; flips with the scheme as the grey does.
- --yeti-color-warning-100 (color): grey-100 at the warning hue — The warning hue at the lightness of --yeti-grey-100; flips with the scheme as the grey does.
- --yeti-color-alert-0 (color): grey-0 at the alert hue — The alert hue at the lightness of --yeti-grey-0; flips with the scheme as the grey does.
- --yeti-color-alert-10 (color): grey-10 at the alert hue — The alert hue at the lightness of --yeti-grey-10; flips with the scheme as the grey does.
- --yeti-color-alert-20 (color): grey-20 at the alert hue — The alert hue at the lightness of --yeti-grey-20; flips with the scheme as the grey does.
- --yeti-color-alert-30 (color): grey-30 at the alert hue — The alert hue at the lightness of --yeti-grey-30; flips with the scheme as the grey does.
- --yeti-color-alert-40 (color): grey-40 at the alert hue — The alert hue at the lightness of --yeti-grey-40; flips with the scheme as the grey does.
- --yeti-color-alert-50 (color): grey-50 at the alert hue — The alert hue at the lightness of --yeti-grey-50; flips with the scheme as the grey does.
- --yeti-color-alert-60 (color): grey-60 at the alert hue — The alert hue at the lightness of --yeti-grey-60; flips with the scheme as the grey does.
- --yeti-color-alert-70 (color): grey-70 at the alert hue — The alert hue at the lightness of --yeti-grey-70; flips with the scheme as the grey does.
- --yeti-color-alert-80 (color): grey-80 at the alert hue — The alert hue at the lightness of --yeti-grey-80; flips with the scheme as the grey does.
- --yeti-color-alert-90 (color): grey-90 at the alert hue — The alert hue at the lightness of --yeti-grey-90; flips with the scheme as the grey does.
- --yeti-color-alert-100 (color): grey-100 at the alert hue — The alert hue at the lightness of --yeti-grey-100; flips with the scheme as the grey does.
- --yeti-color-neutral-0 (color): grey-0 at the neutral hue — The neutral hue at the lightness of --yeti-grey-0; flips with the scheme as the grey does.
- --yeti-color-neutral-10 (color): grey-10 at the neutral hue — The neutral hue at the lightness of --yeti-grey-10; flips with the scheme as the grey does.
- --yeti-color-neutral-20 (color): grey-20 at the neutral hue — The neutral hue at the lightness of --yeti-grey-20; flips with the scheme as the grey does.
- --yeti-color-neutral-30 (color): grey-30 at the neutral hue — The neutral hue at the lightness of --yeti-grey-30; flips with the scheme as the grey does.
- --yeti-color-neutral-40 (color): grey-40 at the neutral hue — The neutral hue at the lightness of --yeti-grey-40; flips with the scheme as the grey does.
- --yeti-color-neutral-50 (color): grey-50 at the neutral hue — The neutral hue at the lightness of --yeti-grey-50; flips with the scheme as the grey does.
- --yeti-color-neutral-60 (color): grey-60 at the neutral hue — The neutral hue at the lightness of --yeti-grey-60; flips with the scheme as the grey does.
- --yeti-color-neutral-70 (color): grey-70 at the neutral hue — The neutral hue at the lightness of --yeti-grey-70; flips with the scheme as the grey does.
- --yeti-color-neutral-80 (color): grey-80 at the neutral hue — The neutral hue at the lightness of --yeti-grey-80; flips with the scheme as the grey does.
- --yeti-color-neutral-90 (color): grey-90 at the neutral hue — The neutral hue at the lightness of --yeti-grey-90; flips with the scheme as the grey does.
- --yeti-color-neutral-100 (color): grey-100 at the neutral hue — The neutral hue at the lightness of --yeti-grey-100; flips with the scheme as the grey does.