# Surface Layout Spec, v0

- **Status:** draft
- **Edition:** none yet. Version 0 has no stability guarantee.
- **Related documents:** [Language spec v0](https://surface.solenovis.com/docs/language.md) (section 12), [component reference](https://surface.solenovis.com/docs/components.md), [ADR-0005](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/adr/0005-css-subset-as-layout-model.md), [PLAN.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/PLAN.md) (section 10)
- **Changes:** 2026-11-06: a press hits the shown position of a transform, a spring uses its `linear()` stops, the keys scroll a `Scroll` with the focus, `outline-none` removes the ring, and Linux follows a change of reduced motion, on the native targets too (5.10, 5.12, 12, ADR-0131). 2026-11-06: the compiler has the cues of a sequence on all targets (5.10, 12, ADR-0152). Each element of a cue has the length of its sequence on the web, so all elements turn together. 2026-11-06: the names of the styles (2.1, 4, 5.6, 5.7, 5.9, 5.10, 10, 11, 12, ADR-0161). `active:`, `focus-visible:`, `inset-s-`, `inset-e-`, `grid-cols-`, `grid-rows-`, `grid-areas-`, `overflow-hidden`, `overflow-visible`, `wrap-anywhere`, `wrap-normal`, `font-sans`, `stroke-dash-`, `stroke-cap-`, `stroke-join-`, `mask-ellipse` and `shadow-xs`, and the keys `color`, `length`, `font`, `breakpoint` and `font-weight` of `style.toml`. Each old name is a compile error that names the new name. `shadow-sm` has the value of Tailwind 4. The property `clip` is `overflow`. A form of Tailwind with another meaning, for example `scale-95`, is a compile error. The spec lists `forced-colors:`, a computed `tracking-` and the rules of the names of `keyframes`. 2026-11-06: the proposals of the website findings that fit (5.1, 5.5, 5.7, 5.8, 5.9, 5.10, 12, ADR-0160): `pointer-events-auto`, `visible`, `invisible`, `aspect-auto`, `origin-`, `skew-x-`, `skew-y-`, `object-<position>`, `indent-`, `decoration-`, `underline-offset-`, `font-stretch-`, `auto-cols-`, `caret-`, `placeholder-`, `accent-`, the cursors of CSS, `group-<state>:`, `peer-<state>:`, `focus-within:`, `motion-reduce:`, `rtl:` and `ltr:`. `pointer-events` inherits, as in CSS. `indent-` is not in version 0 on the native targets. 2026-11-06: the compiler has the split of a `Text` into lines, words and letters on all targets, with `split-clip` (5.10, 12, ADR-0153). The native targets find the words with the ICU of the system. 2026-11-06: the compiler has the animations that follow the scroll on all targets (5.10, 12, ADR-0151). A browser without `animation-timeline` gets their progress from the runtime. A timeline name takes no variant, and a range has no effect on the time, as in CSS. 2026-11-06: the compiler has `animate-delay-` and the fill `both` of each animation on all targets (5.10, 12, ADR-0150). An animation that has ended keeps its last stop and uses no frames. 2026-11-06: the effects of version 1 on all targets (5.7, 5.8, 5.9, 5.10, 9, 10, 12, ADR-0145). A text shadow with the sizes of Tailwind 4, the blur of a backdrop, the blend modes of CSS and `isolate`. `mask` gets the shapes of CSS `clip-path`: an inset with corners, a circle, an ellipse and a polygon. A shape can have a transition, and it limits the hit area, as `clip-path` does. 2026-11-06: the compiler has gradients, the four shadow layers and background images on all targets (2, 2.1, 5.8, 10, 12, ADR-0127). One declaration of `background-image` holds the gradient and the picture, and its parts are the parts of the gradient and the picture. The native renderers have a pipeline for each kind of gradient. 2026-11-06: the compiler has the lengths of `Theme.setToken` and the check of the kind of its value on all targets (2.1, 10, ADR-0130). A length token that no call changes is a constant of the build. 2026-11-05: the compiler has the selection of text on all targets (5.10, 10, 12, ADR-0131). The text of a field stays selectable, also with `select-none`, as CSS UI 4 says. `select-auto` is a compile error. 2026-11-05: the filters and a `fixed` element under a filter work on the native targets (5.7, 5.8, ADR-0131). 2026-11-05: the design of motion for version 1 (P6.24, P6.27, ADR-0150 to ADR-0154). Sections 5.10 and 12 get the delay and the fill of an animation, and the animations that follow the scroll. They also get the cues of a sequence, the split of a text and the transition names of a page. The spec marks the new features "Not in version 0". 2026-11-05: the compiler has `sticky` on the native targets, scroll snapping on all targets, and the paint order of `z-` on the native targets (5.7, 5.12, 9, 12, 12.3, ADR-0128). The native targets end a scroll gesture as the browsers do (5.12). 2026-11-05: the compiler has the answers of 2026-11-05 on all targets. `dir-ltr` and `dir-rtl` write the attribute `dir`, and `mask-fade-` has the logical edges. A variant wins over the base. An unknown area is an error, and a placement can have a name. The attribute of a `dir-` utility with a state, a theme or a window width follows the variant (5.6, 5.7, 5.9, 10, 12.3, 12.4, ADR-0124, ADR-0126). 2026-11-05: the answers of the project owner for version 1 (13). `dir-ltr` and `dir-rtl` write the attribute `dir`, and a logical fade uses `:dir(rtl)` (5.9, 10, 12.4, ADR-0124). A variant always wins over the base, also for a physical side and a logical side (12.3, ADR-0124). An unknown name in `area-` is a compile error, and `col-span-<name>` and `col-<n>_<name>` place an item at a named line (5.6, 12.4, ADR-0126). The spec marks the new rules "Not in version 0". 2026-11-05: the compiler has named lines, areas, `auto-fill`, `auto-fit` and dense on all targets. `flow-row` and `flow-col` are gone (5.5, 5.6, 12.4, ADR-0126). 2026-11-04: the compiler has the logical sides on all targets, and a utility of all sides or of the axis `x` also sets the logical sides (3, 5.2, 12.1, 12.3, 12.4, ADR-0124). 2026-11-04: `select-none` on an `Input` or a `TextArea` turns off the selection of the field, and `surf lint` reports it (5.10, 12, ADR-0131). 2026-11-04: the answers of the project owner to the details of version 1. A value of the wrong kind in `Theme.setToken` (2.1, ADR-0130). Computed values of `start-` and `end-`, the logical edges of `mask-fade-`, and the physical `translate-x-` and directions of a gradient (5.7, 5.8, 12, ADR-0124). `grid-flow-row` and `grid-flow-col` (5.5, 12, ADR-0126). The utilities that reset a value (5.8, 5.12, 12, ADR-0127, ADR-0128). The distance of `snap-proximity` (5.12, ADR-0128). Text is not selectable without `select-text` (5.10, 10, 12, ADR-0131). One behavior on all targets for `fixed` under a filter, the filters, a theme change during a transition and the stops of a spring (5.7, 5.8, 5.10, ADR-0131). One behavior on all targets for reduced motion on Linux, line height `normal`, the line breaks of ASCII text and `outline-none` (5.10, 7, 12, ADR-0131). 2026-11-04: the decisions for version 1 replace the open questions (13). Logical sides (3, 5.2, 5.7, 5.8, 5.9, 10, 12, ADR-0124). The rules of percentages (2, ADR-0125). Named lines, areas, `auto-fill`, `auto-fit` and dense (5.5, 5.6, 12, ADR-0126). Gradients, shadow layers and background images (2, 2.1, 5.8, 5.10, 10, 12, ADR-0127). `sticky` on the native targets, and scroll snapping (5.7, 5.12, 9, 12, ADR-0128). No reverse flex directions (5.3, 6, ADR-0129). Theme lengths at run time (2.1, 10, ADR-0130). The spec marks the new features "Not in version 0". 2026-11-03: a block has no wrapper element on the web, also a `#for` next to other nodes, an instance with fills and a `@slot` at the top of a fragment (10, ADR-0121). 2026-11-03: the parts of a property merge when two declarations both apply, also for two conditions (10, 12.3, ADR-0120). 2026-11-03: a condition takes the parts of `style` (12.3, ADR-0118). 2026-11-03: `font-mono` on the web takes `monospace` after `ui-monospace`, which only Safari knows (5.9, ADR-0112). 2026-11-03: `Span` and a `Link` in a `Text`: the paragraph of several runs (4, 7, 10, ADR-0111). 2026-11-03: `fixed`, `group`, `group-hover:` and the filters of the web (5.7, 5.8, 12, ADR-0106), the full token set and the high contrast variant (2.1, ADR-0103), and computed transforms, parts of `transform` that merge, named easings, springs, `delay-`, and the setting of reduced motion (5.8, 5.10, 10, 12, ADR-0104). 2026-11-02: a token always has `$` (2, ADR-0090), TOML for the theme and style presets (2.1, 2.2, ADR-0091, ADR-0092), conditions in style text (12, ADR-0096), named breakpoints (12.2, ADR-0097) and a spacing scale (12.1, ADR-0098). 2026-11-01: lengths with a unit (2, ADR-0067), `dir` and the layout of right-to-left text (3, 5, ADR-0066), and text marks (5, ADR-0068). 2026-10-31: `animation` and the `animate-` utilities (5.10, ADR-0049). 2026-10-30: all targets animate `transition` (5.10, ADR-0035). 2026-10-29: the default `min-height` is `auto`, as in CSS: a flex item keeps its content height, and a `Scroll` or `List` can shrink to 0 (section 3, ADR-0028). `min-width` stays 0. 2026-10-28: utilities replace the `<style>` section and the style attributes (section 12, ADR-0020). `Box` is removed. Section 5 is the model that the utilities expand to. 2026-10-26: P1.9 adds the theme tokens (2.1) and the stylesheet of the web mapping (10). 2026-10-25: section 11 names the CSS variables for the test font. 2026-10-25: `line-height` takes `normal` or a number, not a length (section 5.9). The value check of P1.1 found the conflict with section 2.

This document specifies the layout and style model of Surface. It covers the units, the box model, the layout of each built-in component, every style property and the utilities that set them. For each property, it also gives the CSS property (web) and the Taffy 0.15 field (native).

A developer writes styles as utilities in the `style` attribute (section 12). The compiler expands the utilities to declarations of the properties of section 5. The web emitter and the native runtime implement these declarations.

The design rule: **one model, two implementations, the same boxes.** The browser implements this model with CSS. The native runtime implements it with Taffy. Both are CSS implementations, so this spec is a subset of CSS with fixed defaults. A property or value that the two implementations cannot give in the same way is not in the spec. The compiler rejects every property and value that is not in this document.

---

## 1. Conventions

- **must** and **must not** state a rule. A violation of a rule is a compile error, unless the rule says otherwise.
- "Logical px" is the unit of all layout values. The runtime multiplies by the scale factor of the screen to get device pixels. The web emitter writes a plain number in `rem`, as the number over 16. So a browser with a larger default font size scales the app. Borders and shadows stay in `px` (ADR-0067).
- "CSS" in the mapping columns is the CSS property that the web emitter writes. "Taffy" is the field of `taffy::Style` that the native runtime sets.
- The native runtime must set every `taffy::Style` field from this spec. It must not depend on a Taffy default, because a Taffy default can change in a new Taffy version.

---

## 2. Values

| Value | Syntax | Example | Valid in |
|---|---|---|---|
| Length | A number in logical px, or a number with a unit (ADR-0067) | `24`, `0.5`, `1.5rem`, `50vw` | All length properties |
| Percentage | A number with `%` | `50%` | `width`, `height`, `min-*`, `max-*`, `basis`, `inset`, and the position of a gradient stop (ADR-0127) |
| Fraction | A number with `fr` | `1fr` | Grid tracks only |
| `auto` | The keyword | `auto` | Where the table of the property lists it |
| Integer | A number without a fraction | `2` | `z-index`, `max-lines`, `span` |
| Number | A number | `0.8` | `grow`, `shrink`, `opacity`, `line-height`, `aspect-ratio` |
| Color | `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb(r g b / a)`, `transparent` | `#007aff` | Color properties |
| Token | `$name` | `$accent` | Wherever the type of the token fits |
| Duration | A number with `ms` | `200ms` | `transition`, `animation` |
| Angle | A number with `deg` | `45deg` | `transform`, and the angle of a gradient (ADR-0127) |
| Image | `url(<path>)` | `url(assets/hero.png)` | `background-image` (ADR-0127) |

Rules for values:

- Percentages refer to the size of the parent's content box in the same axis.
- `padding`, `margin` and `gap` never take a percentage (ADR-0125). CSS computes a vertical padding in percent from the width, and this surprises many developers. `aspect-ratio` already replaces the old trick of a padding in percent.
- `radius` and `border` take no percentage in version 1.
- A color is an sRGB color. All blending happens on sRGB values, in the same way on all targets (as browsers do).
- A token comes from the theme (section 2.1). The type of the token must fit the property: a color token for `background`, a length token for `padding`. An unknown token is a compile error.
- A token always has `$`: `bg-$accent`, `p-$gutter`. A token can change with the theme or while the app runs. A token with no `$` is a compile error, and the error gives the correct form (ADR-0090).
- A constant never has `$`: `p-3`, `text-xs`, `gap-inset`. The compiler replaces a constant with its value.
- A length can have the unit `rem`, `em`, `vw`, `vh`, `vmin` or `vmax`, or be `min()`, `max()`, `clamp()` or `calc()` of lengths (ADR-0067). `rem` is 16 logical px on the native targets. In a utility, `_` is a space: `w-calc(100vw_-_2rem)`.
- `calc()` takes no percentage in version 1. A minor version after version 1 adds percentages in `calc()`, for example `w-calc(100%_-_2rem)` (ADR-0125).
- The web output writes a plain number in `rem`, so the font size of the browser scales the app. Borders and shadows stay in `px`.
- Named colors other than `transparent` are not valid in v0.

### 2.1 Theme tokens

These are the default tokens. A color token has four values: light, dark, high contrast light and high contrast dark (ADR-0103). The system selects the variant on each target that has the settings: its dark appearance gives the dark value, and its setting for more contrast gives a high contrast value. `Theme.setMode` also selects it. A length token has the same value in all variants.

A project can change them with `src/style.toml` (ADR-0034): new values, new tokens, fonts and the fit of the UI to the window. The compiler reads the file, so a new token is checked as a default token is, and a wrong name is a compile error. The values are fixed at compile time.

```toml
step = "0.25rem"

[color]
accent = ["#9f40ff", "#d0bcff"]
tertiary = ["#7d5260", "#efb8c8"]
outline = "#79747e"

[length]
radius = 12
space-huge = 32

[spacing]
inset = 12

[font]
sans = "assets/Inter.ttf"
display = "assets/Display.ttf"

[breakpoint]
md = 700

[scale]
design = [460, 674]
min = 0.75
max = 2.5

[keyframes.fade]
ms = 500
ease = "ease-linear"
0 = "opacity-1"
100 = "opacity-0"
```

- A color is a text, a pair of the light and the dark value, or these two with the two high contrast values: `accent = ["#9f40ff", "#d0bcff", "#5a00b0", "#e8dcff"]`. A pair keeps its values in high contrast. A length is a number in logical px. A name of the default theme keeps its kind.
- A token that a project leaves out follows the tokens that it sets. For example, `$accent-hover` and `$focus` follow `$accent`, and `$on-accent` is black or white, the one with the larger contrast.
- `surf lint` checks the pairs of text and background tokens against WCAG AA in each variant (rule `contrast`, ADR-0103).
- `color` and `length` hold tokens. `spacing`, `radius`, `text`, `shadow`, `font-weight`, `leading`, `tracking` and `container` hold constants (ADR-0053). `container` holds widths for `max-w-`, after the names of `spacing`. `spacing` holds lengths for the utilities of the scale (section 12.1) and for `mask-fade-`. `shadow` has the sizes `xs`, `sm`, `md`, `lg` and `xl`. Each size is the first layer of the size of Tailwind 4, without its spread. `inset-shadow` holds the sizes of the inset shadow in the same way, with the defaults `2xs`, `xs` and `sm` of Tailwind 4 (ADR-0127). If a token and a constant have the same name, a bare name is the constant and `$name` is the token.
- A font has a name and a file in the assets, and the utility `font-<name>` selects it. `sans` and `mono` replace the two default fonts. The name `system` is a compile error that names `sans` (ADR-0161). A name has one file: a variable font gives all weights.
- `keyframes` adds a looping animation for the utility `animate-<name>` (ADR-0049). Each number is a stop in percent. A stop holds utilities of the properties that can have a transition (5.10). Each stop sets the same properties. `ms` is the time of one round, and the default is 1000. `ease` is the name of an easing utility of section 12.4, as the `ease` of a `Transition` (component reference 4.15): `ease-linear`, `ease-out-cubic`. The default is `ease`. A name without `ease-`, for example `linear`, is a compile error that gives the correct name. An unknown name in `animate-<name>` is a compile error.
- The name of an animation cannot be `none` or `infinite`. It cannot start with `delay-`, `timeline-`, `range-`, `duration-`, `ease-` or `count-`, because `animate-` with each of these words is a utility (ADR-0150, ADR-0151, ADR-0161).
- `scale` fits the UI to the window. The factor is the smaller of two values: the window width divided by the design width, and the window height divided by the design height. It stays between `min` (0.5 by default) and `max` (4 by default). The lengths of the app stay in design px, also in the pointer events.

| Token | Kind | Light | Dark |
|---|---|---|---|
| `$accent` | color | `#0064d2` | `#4da3ff` |
| `$accent-hover` | color | `#0055b3` | `#73b7ff` |
| `$accent-active` | color | `#004799` | `#94c8ff` |
| `$on-accent` | color | `#ffffff` | `#000000` |
| `$focus` | color | `#0064d2` | `#73b7ff` |
| `$background` | color | `#f2f2f7` | `#000000` |
| `$surface` | color | `#ffffff` | `#1c1c1e` |
| `$hover` | color | `#e8e8ed` | `#2c2c2e` |
| `$active` | color | `#dcdce1` | `#3a3a3c` |
| `$disabled` | color | `#e5e5ea` | `#2c2c2e` |
| `$on-disabled` | color | `#5c5c61` | `#b0b0b5` |
| `$text` | color | `#1c1c1e` | `#f2f2f7` |
| `$text-strong` | color | `#000000` | `#ffffff` |
| `$text-secondary` | color | `#5c5c61` | `#b0b0b5` |
| `$border` | color | `#d1d1d6` | `#38383a` |
| `$border-strong` | color | `#86868b` | `#8e8e93` |
| `$success` | color | `#1a7a35` | `#30d158` |
| `$on-success` | color | `#ffffff` | `#000000` |
| `$success-surface` | color | `#e3f5e8` | `#10321b` |
| `$on-success-surface` | color | `#0f5a24` | `#8fe3a6` |
| `$warning` | color | `#955700` | `#ffb340` |
| `$on-warning` | color | `#ffffff` | `#000000` |
| `$warning-surface` | color | `#fff2d9` | `#38280a` |
| `$on-warning-surface` | color | `#6b3f00` | `#ffd08a` |
| `$error` | color | `#c8251c` | `#ff6961` |
| `$on-error` | color | `#ffffff` | `#000000` |
| `$error-surface` | color | `#fdeaea` | `#3b1513` |
| `$on-error-surface` | color | `#8e1710` | `#ffb4ae` |
| `$font-size` | length | `15` | `15` |
| `$font-size-small` | length | `13` | `13` |
| `$font-size-large` | length | `17` | `17` |
| `$font-size-title` | length | `28` | `28` |
| `$space-small` | length | `4` | `4` |
| `$space` | length | `8` | `8` |
| `$space-large` | length | `16` | `16` |
| `$radius` | length | `8` | `8` |
| `$radius-large` | length | `14` | `14` |

The names of the color tokens follow one pattern: `<role>`, `<role>-<state>`, `on-<role>`, `<role>-surface` and `on-<role>-surface`. A state token with no role, for example `$hover`, is the state of `$surface`. In the same way, `$text` and `$border` are the default text and the default border.

The high contrast values are in `crates/surf-check/src/theme.rs`. The states of a control use these tokens: hover (`$hover`, `$accent-hover`), active (`$active`, `$accent-active`), focus (`$focus`), disabled (`$disabled`, `$on-disabled`), and the three status colors with their text and their surface.

**Tokens at run time.** `Theme.setToken(name, value)` changes a token while the app runs (ADR-0082, ADR-0130). A `Color` changes a color token in each variant. A `Float` changes a length token, in logical px. A name that no token has has no effect. A length that is not finite has no effect.

The kind of the value must fit the kind of the token (ADR-0130).

- With a literal name, a value of the wrong kind is a compile error, for example a `Float` for the color token `accent`: `Theme.setToken("accent", 2.0)`.
- With a computed name, a value of the wrong kind has no effect, as an unknown name has.

These rules apply to a length:

- An app that never gives a length to `setToken` has no run-time code for lengths. Its size does not change.
- With a literal name, only that token can change. All other length tokens stay constants of the build.
- With a computed name, each length token can change.
- On the native targets, a change of a length makes a new layout in the next frame.
- On the web, a length token becomes a CSS variable only when it can change (section 10). The new value is in `rem`, as each length of the web output (section 1).

---

### 2.2 Style presets

A file `style-presets.toml` names style text (ADR-0091). It applies to its folder and to the subfolders of that folder.

```toml
outlined = "border rounded-md p-2"
strong = "@outlined border-2 hover:opacity-0.8"
```

- `@name` uses a preset: `style="@outlined px-4"`, `hover:@outlined`, `{active}:@outlined`.
- The nearest file wins. A subfolder can replace a name of a parent folder.
- A preset holds utilities, variants and other presets. A preset cannot hold a computed value. A cycle of presets is a compile error.
- A utility on the element wins over a utility of the preset if it sets each part that the preset utility sets, with the same variants. An outer preset wins over a preset that it uses in the same way.
- Two presets on one element that set the same part are a compile error.
- The compiler expands a preset, so a preset costs nothing when the app runs.

---

## 3. Box model

- **Border box.** `width` and `height` include the padding and the border. CSS: `box-sizing: border-box`. Taffy: `box_sizing: BorderBox`.
- **Minimum width 0, automatic minimum height.** The default `min-width` is 0, not `auto`: a flex item can shrink below the width of its content. This removes the most common difference between flex implementations (the automatic minimum width of text) and makes text truncation predictable. The default `min-height` is `auto`, as in CSS (ADR-0028):
  - A flex item does not shrink below the height of its content. So a text next to a large `Scroll` keeps its height.
  - A `Scroll` and a `List` can shrink to 0, so they get the height that their siblings leave.
  - An element with a height but without content can shrink below its height, as in CSS.
  - A `Row` or a `Column` that contains a `Scroll` or a `List` is as tall as all their content. To make the `Scroll` or the `List` fit a bounded column, give the container `min-h-0`, as in CSS. A `List` without a bounded height builds all its rows.
- **Every element is a positioning container.** An element with `position: absolute` is placed relative to its parent, not to the nearest positioned ancestor. CSS: every element gets `position: relative` unless it is `absolute`. Taffy places absolute children relative to their parent in all cases.
- **Left to right by default.** `start` means left and `end` means right, unless the element has `dir: rtl`. Then they change places. `dir-rtl` mirrors the layout on all targets. `text-start` and `text-end` follow the paragraph (ADR-0066).
- **Two kinds of sides** (ADR-0124). A physical side is the top, the right, the bottom or the left side. It does not change with the direction. A logical side is the start side or the end side. It follows the direction. Section 5.2 gives the rules. Use a logical side, unless a thing must not mirror, for example a map or a media control.

---

## 4. Built-in components

Each built-in component has a fixed layout mode. Styles can change the properties of the mode, but not the mode.

| Component | Layout mode | Fixed values | CSS (web element) | Taffy |
|---|---|---|---|---|
| `Column` | Flex column | `direction: column` | `<div>`, `display: flex; flex-direction: column` | `Display::Flex`, `FlexDirection::Column` |
| `Row` | Flex row | `direction: row` | `<div>`, `display: flex; flex-direction: row` | `Display::Flex`, `FlexDirection::Row` |
| `Stack` | Grid with one cell | every child in row 1, column 1 | `<div>`, `display: grid`, children `grid-area: 1 / 1` | `Display::Grid`, children `grid_row` and `grid_column` = line 1 |
| `Grid` | Grid | none | `<div>`, `display: grid` | `Display::Grid` |
| `Scroll` | Flex column | `overflow: hidden`, scrolls on the main axis | `<div>`, `display: flex; flex-direction: column; overflow: auto` | `Display::Flex`, `Overflow::Scroll` |
| `List` | Flex column | scrolls on the main axis, only the rows near the visible area exist, the rows do not shrink (ADR-0027) | `<div role="list">`, as `Scroll` | as `Scroll` |
| `Text` | Leaf: text | none | `<span>`, `display: block` | leaf with a measure function (section 7) |
| `Span`, and a `Link` in a `Text` | A part of the text of its `Text` (ADR-0111) | no box of its own for the layout | `<span>` or `<a>`, `display: inline` | a run of the paragraph of its `Text` (section 7.1) |
| `Button` | Flex row | `align: center`, `justify: center` | `<button>` | as `Row` |
| `Toggle` | Flex row | `align: center` | `<label>` with `<input type="checkbox">` | as `Row`, the switch is a leaf |
| `Image` | Leaf: the size and the ratio of its picture (ADR-0033) | none | `<img>` | leaf with `aspect_ratio` |
| `Icon` | Leaf: the ratio of its view (ADR-0030) | without a size from the styles, the height of the font size and the width from the ratio | `<svg viewBox>` with `width` and `height` attributes in `em` | leaf with `aspect_ratio` |
| `Input`, `TextArea`, `Slider`, `Select` | Leaf: control | none | `<input>`, `<textarea>`, `<div>` with a `<button>` or a `<select>` | leaf with a measure function |
| `Overlay` | Flex column, out of the flow (ADR-0032) | no space in its parent, above all other elements | an element with `popover`, `position: fixed` | its own layout pass |
| `Transition` | No box (ADR-0036) | its children take its place | `display: contents` | a fragment |
| `Drawing` | Leaf: the ratio of its view (ADR-0029) | without a width from the styles, the width of the view. The shapes take no part in the layout | `<svg viewBox>` with the `width` attribute | leaf with `aspect_ratio` |
| `Canvas`, `PlatformView` | Leaf: size from styles only | none | `<canvas>`, none | leaf |

---

## 5. Property reference

The **Inh.** column says if a child gets the value of its parent when it has no value of its own. All other properties start from their default on each element.

A row with "(not in version 0)" after the name of its property is a feature that the compiler does not have yet. It gets its fixtures when the compiler gets the feature (section 11).

### 5.1 Size

| Property | Values | Default | Inh. | CSS | Taffy |
|---|---|---|---|---|---|
| `width` | length, %, `auto` | `auto` | no | `width` | `size.width` |
| `height` | length, %, `auto` | `auto` | no | `height` | `size.height` |
| `min-width` | length, % | `0` | no | `min-width` | `min_size.width` |
| `min-height` | length, % | automatic: the content height, 0 for a Scroll and a List (section 3, fixture MinSize) | no | `min-height` | `min_size.height` |
| `max-width` | length, %, `none` | `none` | no | `max-width` | `max_size.width` (`auto` for `none`) |
| `max-height` | length, %, `none` | `none` | no | `max-height` | `max_size.height` (`auto` for `none`) |
| `aspect-ratio` | number, `a / b`, or `auto` (ADR-0160) | none | no | `aspect-ratio` | `aspect_ratio` (`None` for `auto`) |

### 5.2 Space

| Property | Values | Default | Inh. | CSS | Taffy |
|---|---|---|---|---|---|
| `padding` | 1 to 4 lengths (top, right, bottom, left, as in CSS) | `0` | no | `padding` | `padding` |
| `margin` | 1 to 4 lengths or `auto` | `0` | no | `margin` | `margin` |
| `gap` | 1 or 2 lengths (row gap, column gap) | `0` | no | `gap` | `gap` |

A `gap` value of one length applies to both axes.

**Logical sides** (ADR-0124).

- `padding`, `margin`, `inset` and `border-width` have two logical sides: `start` and `end`.
- `radius` has four logical corners. `start-start` is the corner of the top side and the start side. `start-end` is the top and the end, `end-start` is the bottom and the start, and `end-end` is the bottom and the end, as in CSS.
- `start` is the left side and `end` is the right side, unless the element has the direction `rtl`. Then they change places. The direction comes from `dir` (section 5.9).
- A physical side wins over a logical side, and a logical side wins over an axis (section 12.3).
- The web writes the logical properties of CSS: `padding-inline-start`, `padding-inline-end`, `margin-inline-start`, `margin-inline-end`, `inset-inline-start`, `inset-inline-end`, `border-inline-start-width`, `border-inline-end-width`, `border-start-start-radius`, `border-start-end-radius`, `border-end-start-radius` and `border-end-end-radius`.
- The native targets give the physical side to Taffy from the direction of the element. The renderer gets the physical corners in the same way.

### 5.3 Flex container (`Column`, `Row`, `Scroll`, `List`, `Button`, `Toggle`)

| Property | Values | Default | Inh. | CSS | Taffy |
|---|---|---|---|---|---|
| `align` | `start`, `center`, `end`, `stretch`, `baseline` | `stretch` | no | `align-items` | `align_items` |
| `justify` | `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly` | `start` | no | `justify-content` | `justify_content` |
| `wrap` | `nowrap`, `wrap` | `nowrap` | no | `flex-wrap` | `flex_wrap` |
| `align-content` | `start`, `center`, `end`, `stretch`, `space-between`, `space-around` | `stretch` | no | `align-content` | `align_content` |

The flex direction comes from the component (section 4). `flex-row` and `flex-col` change it. There are no reverse directions, and they never come (ADR-0129). A reversed row shows the children in an order that is different from the reading order and the focus order. `dir-rtl` mirrors a row, and an app can reverse its list in code: `items.reversed()`.

### 5.4 Flex item (a child of a flex container)

| Property | Values | Default | Inh. | CSS | Taffy |
|---|---|---|---|---|---|
| `grow` | number ≥ 0 | `0` | no | `flex-grow` | `flex_grow` |
| `shrink` | number ≥ 0 | `1` | no | `flex-shrink` | `flex_shrink` |
| `basis` | length, %, `auto` | `auto` | no | `flex-basis` | `flex_basis` |
| `align-self` | `auto`, `start`, `center`, `end`, `stretch`, `baseline` | `auto` | no | `align-self` | `align_self` (`None` for `auto`) |

The utility `grow` without a value means `grow: 1`.

### 5.5 Grid container (`Grid`)

| Property | Values | Default | Inh. | CSS | Taffy |
|---|---|---|---|---|---|
| `columns` | track list | none (one implicit column) | no | `grid-template-columns` | `grid_template_columns` |
| `rows` | track list | none (implicit rows) | no | `grid-template-rows` | `grid_template_rows` |
| `auto-rows` | one track size | `auto` | no | `grid-auto-rows` | `grid_auto_rows` |
| `auto-columns` | one track size (ADR-0160) | `auto` | no | `grid-auto-columns` | `grid_auto_columns` |
| `areas` | rows of names of cells (ADR-0126) | none | no | `grid-template-areas` | `grid_template_areas` |
| `auto-flow` | `row`, `column` | `row` | no | `grid-auto-flow` | `grid_auto_flow` |
| `auto-flow` | `dense`, `row dense`, `column dense` (ADR-0126) | `row` | no | `grid-auto-flow` | `grid_auto_flow` |
| `align` | `start`, `center`, `end`, `stretch` | `stretch` | no | `align-items` | `align_items` |
| `justify-items` | `start`, `center`, `end`, `stretch` | `stretch` | no | `justify-items` | `justify_items` |
| `justify` | as in 5.3 | `start` | no | `justify-content` | `justify_content` |

A **track list** is a list of track sizes, separated by spaces. A track size is one of these:

- a length, a percentage or a fraction (`1fr`)
- `auto`, `min-content` or `max-content`
- `minmax(<min>, <max>)` of two of the values above (the minimum cannot be a fraction)
- `repeat(<n>, <track list>)` with an integer `n`
- `repeat(auto-fill, <track list>)` or `repeat(auto-fit, <track list>)` (ADR-0126)

A name of a line goes in brackets before or after a track: `[main]` (ADR-0126).

**Named lines, areas, `auto-fill`, `auto-fit` and dense** (ADR-0126). The rules of CSS apply, and Taffy implements them:

- `auto-fill` repeats the tracks as often as they fit. `auto-fit` does the same, and then the tracks with no item collapse to 0.
- A track list has at most one `repeat()` with `auto-fill` or `auto-fit`. Each track of that list has a fixed size. A fixed size is a length, a percentage, or `minmax()` with a length or a percentage as one of its two values.
- `areas` names the cells of the grid, one row after the other. Each row has the same number of cells. The cells of one name make a rectangle. A `.` is a cell with no name. An area that breaks these rules is a compile error.
- An area `nav` also names its lines `nav-start` and `nav-end`.
- `dense` fills a hole of the grid with a later item. So the order on the screen can be different from the order of the markup and of the focus.
- Subgrid comes after version 1, when Taffy supports it.

The utilities of `auto-flow` have the names of Tailwind 4: `grid-flow-row`, `grid-flow-col`, `grid-flow-dense`, `grid-flow-row-dense` and `grid-flow-col-dense` (section 12.4, ADR-0126). They replace the names `flow-row` and `flow-col` of version 0, which are gone.

### 5.6 Grid item (a child of a `Grid`)

| Property | Values | Default | Inh. | CSS | Taffy |
|---|---|---|---|---|---|
| `column` | `<start>`, `<start> / <end>`, `span <n>`, `<start> / span <n>`. A line can be `<n> <name>`, and a span can be `span <name>` (ADR-0126) | automatic | no | `grid-column` | `grid_column` |
| `row` | as `column` | automatic | no | `grid-row` | `grid_row` |
| `area` | the name of an area of the parent (ADR-0126) | automatic | no | `grid-area` | `grid_row` and `grid_column`, with the lines of the area |
| `align-self` | as in 5.4, without `baseline` | `auto` | no | `align-self` | `align_self` |
| `justify-self` | `auto`, `start`, `center`, `end`, `stretch` | `auto` | no | `justify-self` | `justify_self` |

Line numbers start at 1. Negative line numbers are not valid in v0.

A line can also be a name (ADR-0126). The rules of CSS apply:

- `col-main` starts at the first line with the name `main`, and it spans one track.
- With the name of an area, `col-nav` spans the columns of the area `nav`, from `nav-start` to `nav-end`.
- `area-nav` places the item in the rows and the columns of the area `nav`.
- `col-2_main` starts at the second line with the name `main` (`2 main`). When the grid has fewer such lines, each implicit line counts as a line with the name, as in CSS.
- `col-1_/_span_main` starts at line 1 and spans to the next line with the name `main` (`span main`). A span to a name with no line before it, for example `col-span-main` alone, is a span of one track, as in CSS.
- An `area-<name>` with a name that no `grid-areas-` utility of the parent has, with or without a variant, is a compile error. The check applies only when the parent is in the same file. A parent in another file gets no check, for example a `Grid` around the root of a component.

### 5.7 Position and order

| Property | Values | Default | Inh. | CSS | Taffy |
|---|---|---|---|---|---|
| `position` | `relative`, `absolute`, `sticky`, `fixed` | `relative` | no | `position` | `position`. `sticky` is `relative`, and the native scroll code moves the element when it paints (ADR-0128). `fixed` is `absolute`, with the root as its parent |
| `direction` | `row`, `column` | by element | no | `flex-direction` | Taffy `flex_direction` |
| `display` | `none` | by element | no | `display` | Taffy `Display::None`, and the renderer skips the element |
| `pointer-events` | `none`, `auto` | `auto` | yes | `pointer-events` | a hit test skips an element with `none`. A child gets the value, and `auto` lets it take the pointer again (ADR-0160) |
| `visibility` | `visible`, `hidden` | `visible` | yes | `visibility` | an element with `hidden` keeps its box, and it paints nothing of its own. It gets no pointer and no focus, and it is not in the accessibility tree. A child with `visible` paints (ADR-0160) |
| `inset` | 1 to 4 lengths, %, or `auto` (top, right, bottom, left), and the logical sides of section 5.2 | `auto` | no | `top`, `right`, `bottom`, `left` | `inset` |
| `z-index` | integer | `0` | no | `z-index` | none (paint order, section 9) |
| `overflow` | `visible`, `hidden` | `visible` | no | `overflow` | `overflow` = `Hidden` for `hidden`, and the renderer clips |
| `object-fit` | `fill`, `contain`, `cover`, `none` | `fill` | no | `object-fit` | how the picture of an `Image` fills its box |
| `object-position` | `center`, `top`, `bottom`, `left`, `right`, `left top`, `right top`, `left bottom`, `right bottom` | `center` | no | `object-position` | the place of the picture of an `Image` in the free space of its box (ADR-0160) |
| `mask` | `none`, `ellipse`, or the word fade and 4 lengths (top, right, bottom, left). A fade at the start edge or the end edge follows the direction (ADR-0124). Or a shape: `inset(<sides> round <radii>)`, `circle(<radius> at <x> <y>)`, `ellipse(<rx> <ry> at <x> <y>)` or `polygon(<x> <y>, ...)` (ADR-0145) | `none` | no | `mask-image`, and `clip-path` for a shape | the mask table of the renderer (ADR-0065): a rounded box, an ellipse, or a tile of coverage for a polygon |
| `group` | `on` | none | no | the class `s-group` | the variants `group-<state>:` of the descendants look for the class |
| `peer` | `on` | none | no | the class `s-peer` | the variants `peer-<state>:` of the later siblings look for the class (ADR-0160) |

An absolute element does not take space in its parent. Its position comes from `inset`, relative to the padding box of the parent.

A `fixed` element has its place in the window (ADR-0106):

- It takes no space in its parent. It does not move with a `Scroll`, and a clip of an ancestor does not limit it.
- It must set a place on both axes: `top-` or `bottom-`, and `inset-s-`, `inset-e-`, `left-` or `right-`, or `inset-` (ADR-0124). A missing place is a compile error, because a browser keeps the place in the flow.
- An ancestor with a `transform` or a `filter` takes the place of the window, as in CSS (ADR-0106, ADR-0131). The filter applies to the whole group, so the `fixed` element paints in the layer of the group.
- It paints in tree order (section 9).

**The shapes of a mask** (ADR-0145). A shape of `mask` is a basic shape of CSS `clip-path`. The element and its children paint only inside the shape.

- The box of the shape is the border box of the element, as the default box of `clip-path`. A percentage of x is a percentage of the width, and a percentage of y is a percentage of the height.
- `inset(<sides>)` is a rectangle inside the box. The sides are 1 to 4 lengths or percentages, as `padding` has them: top, right, bottom, left. `round <radii>` gives the rectangle 1 to 4 corner radii, as `radius` has them. A radius is a length.
- `circle(<radius>)` is a circle. The radius is a length, a percentage of sqrt(width² + height²) / sqrt(2), `closest-side` or `farthest-side`. A side is the distance from the center to the nearest or the farthest side of the box.
- `ellipse(<rx> <ry>)` is an ellipse. `rx` is a length, a percentage of the width, `closest-side` or `farthest-side`, on the axis x. `ry` is the same on the axis y.
- `at <x> <y>` gives the center of a circle or an ellipse. A value is a length, a percentage, or a word: `left`, `center` or `right` for x, and `top`, `center` or `bottom` for y. One word gives one axis, and the other axis is `center`. Without `at`, the center is the center of the box.
- `polygon(<x> <y>, ...)` has 3 or more points. Each point is two lengths or percentages from the top left corner of the box. The polygon uses the rule `nonzero` of CSS.
- A shape limits the hit area, as `clip-path` does in CSS: a press outside the shape does not reach the element or its children. `ellipse`, a fade and the attribute `mask` do not change the hit area (ADR-0065).
- A shape can have a transition (section 5.10). Two shapes of the same kind mix each value, and two polygons with the same number of points mix each point. Another pair changes at the half of the time, as CSS does for a value that cannot mix.
- An element with a shape is a group (section 9).

A `sticky` element stays in the visible area of its scroll container (ADR-0128). The rules of CSS apply:

- It takes its place in the flow, as a `relative` element does. Its move changes no other box.
- Its scroll container is its nearest ancestor that is a `Scroll` or a `List`, or that has `overflow-hidden`.
- An inset gives its distance from an edge of the visible area of the scroll container. For example, `top-0` keeps it at the top edge. Without an inset, it is `relative`.
- It does not leave the content box of its parent. So the header of a section scrolls away with the end of its section.
- It is a group (section 9), and it paints in tree order. A later sibling paints over it, unless the `sticky` element has a higher `z-`.
- A hit test finds it at the place where it paints.

### 5.8 Visual

| Property | Values | Default | Inh. | CSS | Native renderer |
|---|---|---|---|---|---|
| `background` | color | `transparent` | no | `background-color` | instance fill |
| `background-image` | `none`, or a gradient over a picture: a gradient, `url(<path>)`, or both in this order. A gradient is `linear-gradient(<angle or side>, <stops>)`, `radial-gradient(<stops>)` or `conic-gradient(from <angle>, <stops>)` with two or three stops, each a color and a position (ADR-0127) | `none` | no | `background-image`: the gradient is the first layer, and the picture the last layer | a pipeline for each kind of gradient, and the picture of an `Image` in the box |
| `background-size` | `auto`, `cover`, `contain` | `auto` | no | `background-size` | the scale of the picture |
| `background-position` | `left top`, `center`, a side (`top`, `bottom`, `left`, `right`), or a corner (`right top`, `left bottom`, `right bottom`) | `left top` | no | `background-position` | the place of the picture |
| `background-repeat` | `repeat`, `no-repeat` | `repeat` | no | `background-repeat` | tiles of the picture |
| `border` | `<width> <color>` | `0 transparent` | no | `border: <width> solid <color>` | Taffy `border`, instance border |
| `border-width` | `<top> <right> <bottom> <left> <color>`, and the logical sides of section 5.2 | none | no | `border-style`, `border-width`, `border-color` | Taffy `border`, one strip for each side |
| `radius` | 1 to 4 lengths (top left, top right, bottom right, bottom left), and the logical corners of section 5.2 | `0` | no | `border-radius` | instance radii |
| `shadow` | four layers in this order: the inset shadow, the inset ring, the ring and the shadow. Each layer is `<dx> <dy> <blur> <spread> <color>` with an optional spread, and the word inset comes before an inset layer. A layer without a value is `none`. With only the shadow layer, the value is that layer: `<dx> <dy> <blur> <color>`, or `none` (ADR-0127) | `none` | no | `box-shadow` | shadow instances |
| `opacity` | number from 0 to 1 | `1` | no | `opacity` | layer (section 9) |
| `transform` | a list of `translate(<x>, <y>)`, `scale(<s>)`, `rotate(<angle>)`, `skewX(<angle>)`, `skewY(<angle>)` (ADR-0160) | none | no | `transform` | layer (section 9) |
| `transform-origin` | `center`, `top`, `bottom`, `left`, `right`, `left top`, `right top`, `left bottom`, `right bottom` | `center` | no | `transform-origin` | the point of the box that `scale`, `rotate` and the skew keep (ADR-0160) |
| `filter` | a list of `grayscale(<n>)` from 0 to 1, `brightness(<n>)` of 0 or more, `blur(<length>)` | none | no | `filter` | a layer with a filter: a color matrix, and two passes of a Gaussian kernel for `blur` (ADR-0131) |
| `backdrop-filter` | `none`, or `blur(<length>)` (ADR-0145) | `none` | no | `backdrop-filter`, and `-webkit-backdrop-filter` for older WebKit | a copy of the content behind the element in its box, with the blur of the filters |
| `mix-blend-mode` | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`, `plus-lighter` (ADR-0145) | `normal` | no | `mix-blend-mode` | a layer that the blend pipeline paints over a copy of its group |
| `isolation` | `auto`, `isolate` (ADR-0145) | `auto` | no | `isolation` | the start of a group for the blend modes |

Rules for visual properties:

- Visual properties do not change the layout. A `transform` moves the painted element, not its layout box. The origin of `scale`, `rotate` and the skew is the center of the element, unless `transform-origin` gives another point (ADR-0160).
- A change of a computed transform or a computed `opacity` runs no layout (ADR-0104). The browser changes one custom property or `opacity`. A native target changes the paint values of the element and runs no style pass, unless the element has a `transition`.
- Borders are solid. Each corner radius is limited to half the shorter side of the element, as in CSS.
- `filter` works on all targets, as the CSS property `filter` (ADR-0131). `grayscale` and `brightness` are a color matrix. `blur(<n>)` is a Gaussian blur with a standard deviation of n logical px. Only an app with a filter has the code of the filters. `web:blur-4` stays valid, but an app does not need it.
- `backdrop-filter` blurs the content behind the element, inside the border box of the element with its radius (ADR-0145). The blur is a Gaussian blur with a standard deviation of n logical px, as `blur-`. Then the background, the border and the content of the element paint over it.
- The content behind the element is the content that paints before it, back to its **backdrop root**. This is the rule of Filter Effects 2. The backdrop root is the nearest ancestor with `opacity` below 1, a `filter`, a mask, a `backdrop-filter` or a `mix-blend-mode`, or else the root. A `transform` of an ancestor is not a backdrop root, so the content behind a moved element still counts.
- The blur reads only the inside of the border box. At the edge of the box, the blur reads the content as in a mirror, as the edge mode `mirror` of Filter Effects 2. So a color outside the box does not come into the box.
- `mix-blend-mode` mixes the colors of the element with the content of its group behind it, with the formulas of Compositing and Blending 1. `plus-lighter` adds the two, with the formula of Compositing and Blending 2. A group is the nearest ancestor that is a group (section 9), or the root. The root starts transparent, as a page with no background.
- `isolate` makes the element a group, so a blend mode in it mixes only with its content. `isolation-auto` removes this.
- Only an app with a text shadow, a backdrop, a blend mode or a shape of a mask has their code (ADR-0145).

**Shadow layers** (ADR-0127).

- `shadow` has four layers: `shadow`, `inset-shadow`, `ring` and `inset-ring`. An element has at most one value in each layer.
- Each layer has two parts: a size and a color. A color utility replaces the color of the size, as in Tailwind 4: `shadow-md shadow-$border-strong`.
- The size of `shadow` and of `inset-shadow` is `<dx> <dy> <blur> <spread>`. The spread is optional, and its default is 0. A spread makes the shadow larger on each side before the blur, and a negative spread makes it smaller, as in CSS.
- The size of `ring` and of `inset-ring` is a width. A ring is a shadow with no offset and no blur, and its spread is the width. The default color of a ring is the text color of the element, as in Tailwind 4.
- `shadow` and `ring` paint outside the element, under its background. `inset-shadow` and `inset-ring` paint inside the border, over the background. Each layer follows the radius of the box.
- The edge of a shadow is a Gaussian blur with a standard deviation of `blur / 2`, as CSS `box-shadow` defines it.
- The web writes the four layers in one `box-shadow`. The order is the order of Tailwind 4: the inset shadow, the inset ring, the ring and the shadow. An earlier layer paints over a later layer, as in CSS.
- In an app with an inset layer or a ring, the web writes each `box-shadow` with its four layers. A layer without a value is a shadow of size 0 with no color. So a transition mixes each layer with the same layer, also when a variant adds a ring.
- A spread changes the corner radii as CSS Backgrounds 3 says. Each radius grows by the spread. A radius below the spread grows by the spread times 1 + (r / spread − 1)³.
- A shadow and a ring paint only outside the box, as in CSS. So a box with a half-transparent background does not show the shadow under it.

**Gradients** (ADR-0127).

- `linear` takes an angle or a side: `bg-linear-45` or `bg-linear-to-r`. 0 degrees point up, and the angle turns clockwise, as in CSS. A side to a corner, for example `to-tr`, gives the angle of CSS: the line of the middle color goes through the two other corners.
- `radial` is an ellipse at the center of the box, out to its farthest corner. `conic` turns around the center of the box, and it starts at its angle. The default angle is 0. These are the defaults of CSS.
- A gradient has three stops: `from`, `via` and `to`. Each stop has a color and a position. Without a `via` color, the gradient has only `from` and `to`. `via-none` removes the `via` stop, for example in a variant: `hover:via-none`.
- The directions of `linear` are physical, as in Tailwind 4 (ADR-0124). `bg-linear-to-r` goes to the right, also in an element with the direction `rtl`.
- The default color of a stop is `transparent`. The default positions are 0 %, 50 % and 100 %, as in Tailwind 4.
- A position is a percentage of the gradient line. For `conic`, it is a percentage of the turn.
- The colors mix on premultiplied sRGB values, as in CSS and as in a transition (section 5.10). So a stop of `transparent` fades the color of the next stop with no gray.
- A variant can change one stop, for example `hover:from-$accent` (section 12.3).
- Stops without a kind paint nothing, as in Tailwind 4.
- Each kind of gradient has a shader of its own in the CPU, Metal, Vulkan and Direct3D 11 renderers. Only an app with a kind of gradient has the code and the shader of that kind.
- The parts of `background-image` and of `shadow` merge when two declarations both apply (section 12.3). So `{a}:ring-2 {b}:ring-$error` gives a ring of 2 px in the color of the error while `a` and `b` are both true.

**Background images** (ADR-0127).

- `bg-url(<path>)` paints a picture in the box. The path follows the rules of the `src` of an `Image` (component reference, section 4.5). It is a path in the build or a URL, with the same formats and the same load. On the web, the path in the stylesheet is relative to the stylesheet, which is next to the page.
- A computed `bg-url({path})` gives the picture to each rule of the element that sets `background-image`. So a gradient of a rule paints over it, and `bg-none` of a rule removes it. On the web, the element has the picture in the custom property `--surf-url`.
- The picture has the size of an `Image`: one pixel of the file for each logical px. This is the default, and `bg-auto` sets it. `bg-cover` and `bg-contain` scale it as `object-cover` and `object-contain` scale an `Image`.
- The picture starts at the top left corner, and `bg-top-left` sets this default. `bg-center` puts it in the middle of the box. A side, for example `bg-top`, puts it at the middle of that side. A corner, for example `bg-bottom-right`, puts it in that corner. These are the positions of CSS. They are physical, as in Tailwind 4.
- `bg-repeat` repeats the picture, and this is the default. `bg-no-repeat` paints it one time.
- `bg-none` removes the gradient and the picture, as `background-image: none` does in Tailwind 4. A variant can turn them off in this way: `md:bg-none`.
- `background-size`, `background-position` and `background-repeat` apply to the picture and to the gradient, as one value of CSS applies to each layer. A gradient has no size of its own, so it always fills the padding box.
- The padding box places the picture and the gradient, and the border box is their paint area. So a repeated picture or gradient also paints under the border. The radius of the box clips them. These are the defaults of CSS.
- The color paints first. The picture paints over the color, and the gradient paints over the picture.
- A background image is decoration. It has no alt text, and a screen reader does not find it. A picture with a meaning is an `Image` with `alt`.

### 5.9 Text (`Text`, and the labels of `Button`, `Toggle`, `Select`)

| Property | Values | Default | Inh. | CSS | Native |
|---|---|---|---|---|---|
| `font` | `sans`, `mono`, or a font of the theme | `sans` | yes | `font-family: system-ui`, or `ui-monospace, monospace` (only Safari knows `ui-monospace`) | the platform system font or monospaced font |
| `font-size` | length | `$font-size` (theme, 15 by default) | yes | `font-size` | `surf-text` size |
| `font-weight` | integer from 100 to 900 | `400` | yes | `font-weight` | `wght` axis |
| `line-height` | `normal`, or a number that multiplies the font size | `normal` | yes | `line-height` (unitless) | section 7 |
| `color` | color | `$text` (theme) | yes | `color` | glyph fill |
| `text-align` | `start`, `center`, `end` | `start` | yes | `text-align` | line offset in the text box. `start` is the side where the paragraph starts |
| `font-style` | `normal`, `italic` | `normal` | yes | `font-style` | the italic face of the font: the italic file of the system font, or the `ital` or `slnt` axis. A font with no italic face gets a slant of 14 degrees, the default angle of `oblique` in CSS (ADR-0068, ADR-0131) |
| `text-decoration` | `none`, `underline`, `line-through` | `none` | yes | `text-decoration-line` | a line from the metrics of the font |
| `text-transform` | `none`, `uppercase`, `lowercase`, `capitalize` | `none` | yes | `text-transform` | the case of the shown text. A text control is not affected. |
| `overflow-wrap` | `normal`, `anywhere` | `normal` | yes | `overflow-wrap` | a word that does not fit on a line breaks between two characters |
| `dir` | `auto`, `ltr`, `rtl` | `auto` | yes | `direction`, and `unicode-bidi: plaintext` for `auto`. `ltr` and `rtl` also write the attribute `dir` of HTML on the element (section 10, ADR-0124) | the direction of each paragraph (ADR-0066). `auto` takes it from the first strong character. `ltr` and `rtl` also mirror flex rows and grid columns, with the `direction` of Taffy. The physical sides do not change, and the logical sides follow `dir` (section 5.2, ADR-0124). |
| `text-wrap` | `wrap`, `none` | `wrap` | yes | `white-space: normal` or `nowrap` | line breaking on or off |
| `font-variant-numeric` | `normal`, `tabular-nums` | `normal` | yes | the same | the OpenType feature `tnum`: all digits of the font have the same width, so a number that changes does not move. A font without `tnum` keeps its digits. |
| `font-stretch` | `ultra-condensed`, `extra-condensed`, `condensed`, `semi-condensed`, `normal`, `semi-expanded`, `expanded`, `extra-expanded`, `ultra-expanded`, or a percentage from 50 % to 200 % | `normal` | yes | `font-stretch` | the axis `wdth` of a variable font. A font without the axis keeps its width (ADR-0160) |
| `text-decoration-thickness` | number of logical px | the value of the font | yes | `text-decoration-thickness`, with `inherit` for each element in an app with it | the thickness of an underline and of a line through the text (ADR-0160) |
| `text-underline-offset` | number of logical px | the value of the font | yes | `text-underline-offset` | the place of the top of an underline below the baseline (ADR-0160) |
| `text-indent` (not in version 0) | length | `0` | yes | `text-indent` | the first line starts after the indent, on its start side. Not in version 0: the native targets have no indent (ADR-0160) |
| `caret-color` | color | the text color | yes | `caret-color` | the caret of a text control (ADR-0160) |
| `accent-color` | color | `$accent` | yes | `accent-color` | the box of a `Toggle` and the part of a `Slider` before its knob (ADR-0160) |
| `placeholder-color` | color | `$text-secondary` | no | `color` of `::placeholder` | the placeholder of a text control (ADR-0160) |
| `letter-spacing` | length | `0` | yes | `letter-spacing` | `surf-text` adds it after each glyph |
| `max-lines` | integer, `none` | `none` | no | `-webkit-line-clamp` with `display: -webkit-box` | lines after the limit are cut, and the last line ends with "…" |
| `field-sizing` | `content` | none | no | `field-sizing` on the `<select>` | a `Select` is as wide as its chosen option, not as its widest option |
| `text-shadow` | `none`, or a list of layers `<dx> <dy> <blur> <color>`, separated by commas (ADR-0145) | `none` | yes | `text-shadow` | for each layer, a layer of the text with a filter: the color of the shadow and the blur |

**Text shadows** (ADR-0145).

- `text-shadow` paints a copy of the text under the text for each layer, as in CSS. The copy has the color of the layer, the offset `<dx> <dy>` and a Gaussian blur with a standard deviation of `blur / 2`, as CSS defines it. The first layer paints over the others.
- A shadow is the shape of the glyphs: a color glyph gives a shadow of its shape in the color of the layer. The lines of `underline` and `line-through` have a shadow too. A background of a `Span` has no shadow.
- `text-shadow` has two parts: the size and the color. The color part replaces the color of each layer of the size of the same element, as in Tailwind 4: `text-shadow-md text-shadow-$accent`. The parts merge when two declarations both apply (section 12.3).
- The value inherits, with the color of the element that has the size. A color part alone changes nothing.
- The text of `Input` and `TextArea` has the shadow too, as in CSS. A selection and the caret have no shadow.

### 5.10 Motion

| Property | Values | Default | Inh. | CSS |
|---|---|---|---|---|
| `transition` | `<property> <duration> <easing> <delay>`, separated by commas. The delay is optional | none | no | `transition` |
| `animation` | `<name> <duration> <easing> <rounds>`, or `none` (ADR-0049) | none | no | `animation` |
| `cursor` | `default`, `pointer`, `text`, `grab`, `grabbing`, `ns-resize`, `ew-resize`, `nwse-resize`, `none`, `context-menu`, `help`, `progress`, `wait`, `cell`, `crosshair`, `vertical-text`, `alias`, `copy`, `move`, `no-drop`, `not-allowed`, `all-scroll`, `col-resize`, `row-resize`, `n-resize`, `e-resize`, `s-resize`, `w-resize`, `ne-resize`, `nw-resize`, `se-resize`, `sw-resize`, `nesw-resize`, `zoom-in`, `zoom-out` (ADR-0160) | `default` | no | `cursor` |
| `user-select` | `none`, `text` (ADR-0131) | `none` | yes | `user-select`, and `-webkit-user-select` for WebKit |
| `animation-delay` | a time in ms (ADR-0150) | `0` | no | `animation-delay` |
| `animation-timeline` | `auto`, `scroll`, `view`, each on the axis `y` or `x`, or a name (ADR-0151) | `auto` | no | `animation-timeline` |
| `animation-range` | a start and an end on the timeline (ADR-0151) | `normal` | no | `animation-range` |
| `scroll-timeline` | a name and an axis, on a `Scroll` or a `List` (ADR-0151) | none | no | `scroll-timeline` |
| `view-timeline` | a name and an axis (ADR-0151) | none | no | `view-timeline` |
| `cue` | the name of a cue of a sequence (language spec 7.5a, ADR-0152) | none | no | the animations of the cue |
| `split` | `none`, `lines`, `words`, `letters`, and a clip of each part, on a `Text` or a `Span` (ADR-0153) | `none` | no | the parts of the runtime |
| `view-transition-name` | a name (wave spec 7, ADR-0154) | none | no | `view-transition-name` |

The utilities give all properties of one `transition` the same duration, the same easing and the same delay. Only `opacity`, `transform`, `background`, `color`, `shadow`, the color of `border` and the shape of `mask` can have a transition. `transition` names the first six. The shape of `mask` needs `transition-mask`, as `transition` of Tailwind 4 does not name `clip-path` (ADR-0145). A fade of `mask` changes at the half of the time, as `mask-image` in CSS. Colors change on premultiplied sRGB values. A transition of `shadow` changes its four layers (section 5.8). A layer with no value is then a shadow of size 0 with the color `transparent`, as in CSS. A gradient and a background image have no transition, as in CSS (ADR-0127). A transition never changes the layout during the animation. All targets follow the rules of CSS for a change during an animation (ADR-0035). A new end value starts at the shown value, and a change back takes as long as the way there.

**A theme change and a press during a transition** (ADR-0131). A change of the theme and a new window width animate each property with a `transition`, as each other change of a style does. A press hits the shown position of a transform, not its end position. Not in version 0: a theme change and a new window width apply at once on the native targets.

**Easings.** An easing is one of these (ADR-0104):

- `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`, or `cubic-bezier(a, b, c, d)`. `a` and `c` are numbers from 0 to 1.
- A named curve: `ease-in-<name>`, `ease-out-<name>` or `ease-in-out-<name>`. The names are `sine`, `quad`, `cubic`, `quart`, `quint`, `expo`, `circ` and `back`. The compiler replaces each name with its `cubic-bezier`.
- `spring(<bounce>)`: a spring. `<bounce>` is a number from 0 to 0.8. With 0, the value does not go past its end. A higher number goes past the end and comes back more times. The utility `ease-spring` has the bounce 0.25.

A spring is a curve over the time of its transition, as the other easings are:

- With the time `t` from 0 to 1, `k = 9.24`, `z = 1 - bounce`, `w = k / z` and `d = w * sqrt(1 - z * z)`, the progress is `1 - exp(-k * t) * (cos(d * t) + (k / d) * sin(d * t))`.
- With the bounce 0, the progress is `1 - exp(-k * t) * (1 + k * t)`.
- The curve is 0.1 % or less from its end at `t = 1`, and the progress at `t = 1` is exactly 1.
- The compiler computes the curve as a list of `linear()` stops, with enough stops for an error of 1 % or less (ADR-0131). Each target uses these stops. The web writes them as a CSS `linear()` function. The native targets read them with the rule of `linear()`. A browser without `linear()` uses a `cubic-bezier` with a similar shape.
- A `motion` value keeps the formula on each target.
- A new end value during a spring starts at the shown value with a speed of 0, as with the other easings. Only a `motion` value keeps its speed (language spec 7.5).

**Delay.** During its delay, a property shows its old value. A delay applies each time that the property gets a new end value. A change back keeps the delay, as in CSS.

**The delay and the fill of an animation** (ADR-0150).

- `animate-delay-<ms>` holds back the first round of an `animation`, as `animation-delay` of CSS. The default is 0. A computed value works too: `animate-delay-{i * 150}`.
- An animation shows its first stop during its delay, and its last stop after its last round. This is the fill `both` of CSS. Without a stop at 0 or at 100, that stop is the style of the element (section 2.1). No utility changes the fill.
- The delay starts again each time that the animation starts again, for example when its condition changes from false to true.
- A name of `keyframes` cannot start with `delay-`, `timeline-`, `range-`, `duration-`, `ease-` or `count-` (section 2.1).
- An animation that has ended uses no frames.

**Animations that follow the scroll** (ADR-0151).

An animation runs on a **timeline**. The default timeline is the time. `animate-timeline-` gives the animation a scroll position as its timeline, as `animation-timeline` of CSS. The progress of the animation then follows the scroll position in both directions.

```surf
<Scroll style="h-100vh">
    <Text style="animate-rise animate-timeline-view animate-range-entry">Hello</Text>
</Scroll>
```

| Utility | Timeline | CSS |
|---|---|---|
| `animate-timeline-scroll`, `animate-timeline-scroll-x` | The scroll position of the scroll container of the element, on the axis `y` or `x` | `scroll(nearest y)`, `scroll(nearest x)` |
| `animate-timeline-view`, `animate-timeline-view-x` | The place of the element in the visible area of its scroll container | `view(y)`, `view(x)` |
| `animate-timeline-<name>` | A timeline that an element of the same file names with `scroll-timeline-<name>` or `view-timeline-<name>` | a dashed name, and `timeline-scope` |
| `animate-timeline-auto` | The time, for a variant | `auto` |

- The scroll container of a timeline is the nearest ancestor that is a `Scroll` or a `List`, or that has `overflow-hidden`. This is the rule of `sticky` (section 5.7) and of CSS.
- `scroll-timeline-<name>` goes on a `Scroll` or a `List`, and it names its scroll position. `view-timeline-<name>` goes on an element with a box, and it names its place. The axis is `y`. `scroll-timeline-x-<name>` and `view-timeline-x-<name>` give the axis `x`. A timeline name takes no variant and no condition.
- A name has letters, digits and `-`. It cannot be `auto`, `scroll` or `view`, and it cannot start with `x-`. One element of the file gives the name, and only elements of the same file use it.
- The element with the name and each use need a common ancestor element in the file. A `#for` or a `List` around one of them must also be around the other, so each row has its own timeline. Each other case is a compile error.
- `animate-range-<v>` gives the start and the end of the animation on the timeline, as `animation-range` of CSS. Without it, the animation uses the whole timeline.
  - `animate-range-<name>` is the whole named range: `animate-range-entry`.
  - `animate-range-<start>_<end>` gives the two ends. Each end is a range name with an offset, or an offset alone: `animate-range-entry_25%_cover_50%`, `animate-range-0_400`.
  - An offset is a percentage of the named range, or a length from its start. A number is a length in logical px. An offset alone counts from the start of the whole timeline.
- The named ranges of a view timeline are the ranges of CSS. `o` is the scroll offset, and `V` is the size of the visible area on the axis. `S` and `E` are the start edge and the end edge of the layout box of the element, in the content of the scroll container:

  | Range | Start | End |
  |---|---|---|
  | `cover` | `S - V` | `E` |
  | `contain` | `min(S, E - V)` | `max(S, E - V)` |
  | `entry` | `S - V` | `min(S, E - V)` |
  | `exit` | `max(S, E - V)` | `E` |
  | `entry-crossing` | `S - V` | `E - V` |
  | `exit-crossing` | `S` | `E` |

- The progress is `(o - start) / (end - start)`. The own transform of the element does not change `S` and `E`. Before the start, the element shows the first stop, and after the end, it shows the last stop.
- A scroll timeline goes from the offset 0 to the largest offset. A percentage is a part of that distance. A named range on a scroll timeline is a compile error. A container with no overflow gives no progress, so the element shows its own style, as in CSS.
- An animation on a scroll timeline has one round. `animate-count-`, `animate-infinite`, `animate-duration-` and `animate-delay-` are compile errors with it. The easing of the keyframes applies between the stops, as on the time.
- **Scrub.** The progress follows the scroll position at once, with no smoothing, as in CSS.
- **Pin.** A pinned element is a `sticky` element in a tall parent, with no utility of its own. The view timeline of the parent with the range `contain` gives the progress while the element stays in place:

  ```surf
  <Scroll style="h-100vh">
      <Column style="h-300vh view-timeline-hero">
          <Stack style="sticky top-0 h-100vh">
              <Image src="assets/car.png" alt="A car" style="animate-zoom animate-timeline-hero animate-range-contain" />
          </Stack>
      </Column>
  </Scroll>
  ```

- `on:scroll` gives the position to the program, for example to load more rows. An animation that follows the scroll uses a timeline, so it needs no state and no batch.
- The web writes the CSS of the table. A browser without `animation-timeline` gets the same progress from the runtime: `scrollTimelines` in `surf.js` sets the time of each such animation after each scroll and each new size. Only an app with such an animation has this code.
- The native targets compute the progress when they paint, as for `sticky` (section 5.7). A scroll paints again, and it runs no layout and no style pass. The hit test finds the element at its shown place. The code is behind an install line. Not in version 0: the hit test at the shown place on the native targets. There, a press hits the layout box, as for a transform (ADR-0131).
- With the setting of reduced motion on, an animation on a scroll timeline does not run, and the element shows its own style.

**Sequences** (language spec 7.5a, ADR-0152).

- `cue-<name>` makes the element play a cue of a sequence of its component. The cue gives the keyframes, the time, the easing and the place on the timeline of the sequence.
- An element with `cue-` cannot have `animate-<name>`, `animate-delay-`, `animate-timeline-` or `animate-range-`. A second `cue-` utility, and a `cue-` with a variant or a condition, are compile errors.
- The web plays each cue as a CSS animation. The runtime writes the delay and the time of each element, or its part of the range on a scroll timeline. It turns the direction with the Web Animations API. The native targets run the cues in `animate.rs`, with one clock for each sequence.
- On the web, each animation of a sequence has the length of the sequence. It has a delay before the start of its element, and an end delay after its end. So all animations of a sequence turn together. A browser without `animation-timeline` gets the part of the range of each element in `--surf-slice` (`scrollTimelines` in `surf.js`).
- With the setting of reduced motion on, the stylesheet of the web removes each animation. So the runtime gives each element of a cue with `play` its first stop or its last stop. It writes an animation that does not run into the style attribute of the element.

**Split text** (ADR-0153).

```surf
<Text style="text-48 font-700 split-letters animate-rise">One universal best option</Text>
```

- `split-lines`, `split-words` and `split-letters` go on a `Text` or a `Span`. They divide its text into parts:
  - A letter is an extended grapheme cluster of Unicode (UAX #29). White space is not a part.
  - A word comes from the word boundaries of UAX #29. The web uses `Intl.Segmenter` with the granularity `word`. The native targets and the interpreter use the word break iterator of the ICU of the system (ADR-0132).
  - A line is the text of one line of the paragraph, after the layout. A new width or a new font gives new lines.
- The words:
  - A segment of white space is not a part.
  - A segment that is not like a word, for example punctuation, a symbol or an emoji, belongs to the part before it. So `Hello,` is one part. At the start of the text and after white space, it belongs to the part after it. With white space on both sides, it is a part of its own.
  - A segment is like a word when it has letters, digits or ideographs: `isWordLike` of `Intl.Segmenter`, and the rule status of a word in the break iterator of ICU.
  - Both segmenters get the `lang` of the element or of its nearest ancestor with `lang`. Without a `lang`, they get no language.
  - Chinese, Japanese, Thai, Lao, Khmer and Burmese need a dictionary for their word boundaries. The ICU of each system and of each browser has these dictionaries, so an app carries none.
- The parts have the order of the text, also in right-to-left text. A part can hold the runs of several `Span` elements. In a `Span` with a split, only the text of the `Span` has parts.
- These utilities of the element apply to each part, not to the element. Each other utility stays on the element, for example the color, the font, the padding, the background, the shadow and the filters.

  | Kind | Utilities that apply to each part |
  |---|---|
  | Transform | `translate-x-`, `translate-y-`, `scale-` and `rotate-`, also with `-` before them and with a computed value |
  | Opacity | `opacity-`, also with a computed value |
  | Transition | `transition`, `transition-opacity`, `transition-transform`, `transition-color`, `duration-`, `delay-` and the easings of section 12.4 |
  | Animation | `animate-<name>`, `animate-none`, `animate-duration-`, `animate-ease-`, `animate-count-`, `animate-infinite`, `animate-delay-`, `animate-timeline-` and `animate-range-` |
  | Sequence | `cue-<name>` |
  | Clip | `split-clip` |

- A variant or a condition of these utilities applies too. A state such as `hover:` is the state of the element, not of a part.
- Each part turns and scales around the center of its own box.
- A part paints only its glyphs, so it changes only `opacity`, `transform` and the text color. The keyframes of an animation or of a cue on a split text can set only these. `transition-background`, `transition-shadow` and `transition-border` on a split text are compile errors.
- A part takes its text color from the element. The transition of a part animates a new color of the element.
- A `Span` with a split takes the utilities of the table. A `Span` without a split keeps the utilities of text only (ADR-0111).
- `split-clip` clips each part to its own box. A part that moves up from under the edge of its line then comes into view. It is valid only on an element with a split, and it takes no variant.
- The stagger of a cue gives each part its own start (language spec 7.5a). Without a cue, all parts play at the same time.
- A split changes no box. The text keeps its layout, its line breaks and its line height. Each part paints its glyphs at their place in the text. The box of a part has the height of its line, from the start of its first glyph to the end of its last glyph.
- `split-letters` turns off the ligatures of the text, on all targets. Each letter is then shaped alone. A script that joins its letters, for example Arabic, then shows each letter in its isolated form. Use `split-words` for such a script.
- A screen reader reads a split text as one text, on all targets. The parts are not nodes of the accessibility tree. The selection, the copy and the search of a browser use the text, not the parts.
- A part gets no events. A press goes to the text at its place in the layout, also while a part moves.
- A split in a split is a compile error. The `split-` utilities take no variant and no condition.
- The web keeps the text in its element and paints it transparent. The runtime puts a copy of the parts over it, with `aria-hidden="true"` and `pointer-events: none`. With `split-clip`, each part of the copy is in a box with `overflow: clip`. It measures each part with the rectangles of the text. It measures again after a new size, a new font or a new text (`splitText` in `surf.js`).
- The native targets take the parts from the paragraph (section 7.1), and they paint each part with its own transform, opacity and color. With `split-clip`, each part gets a clip rectangle. Only an app with a split has this code, and only an app with `split-words` has the word segmenter.
- With the setting of reduced motion on, the parts do not move.
- Not in version 0: a split on a `Span`, the underline and the line through of the parts, and a computed `opacity-` of a split text on the web. The checker takes a split on a `Span`, but only the parts of a `Text` paint.

**Page transitions** (wave spec 7, ADR-0154).

- `view-transition-<name>` gives an element a transition name. The old page and the new page can each show one element with the name. That element then moves from its old box to its new box, and its old picture and its new picture cross-fade. A computed name works too: `view-transition-{name}`.
- A name has the scope of the window. Two shown elements with one name stop the transition, and the new page shows at once, as in CSS.
- A fixed name in a row of a `#for` or of a `List` is a compile error, because each row then has the same name.
- The web writes `view-transition-name`. The native targets keep a picture of each named element (wave spec 7).

**Reduced motion.** Each target obeys the setting of the system (ADR-0104):

- With the setting on, a `transition`, an `animation`, an enter, an exit, a move and a stagger do not run. Each element shows its end value at once. A `motion` value goes to its target at once.
- The web target reads `prefers-reduced-motion`. macOS reads the display setting of the accessibility settings. Linux reads `enable-animations` of `org.gnome.desktop.interface` through the settings portal (ADR-0131). Without a portal, Linux reads the settings of GNOME, GTK and KDE at the start of the app.
- A change of the setting applies at once on each target (ADR-0131). On Linux, the settings portal gives `enable-animations` of GNOME and `AnimationDurationFactor` of the group `KDE` of `kdeglobals` (`org.kde.kdeglobals.KDE`). A factor of 0 turns off the animations.
- An app does not write a rule for this.

**Text selection** (ADR-0131).

- Text is not selectable by default, on all targets. `select-text` makes the text of an element and of its children selectable. `select-none` turns it off again for a part, for example for a `Text` in a `Column` with `select-text`.
- In selectable text, a drag selects text, also across the parts of a `Text` and across `Text` elements. A selection does not hold the text of a part with `select-none`. A double click selects a word, and a triple click selects a paragraph. A drag after a double click or a triple click selects whole words or paragraphs. Shift and a click, or Shift and an arrow key, move the moving end of the selection. With Alt on macOS, and with Control on the other systems, an arrow key moves it by a word.
- The copy key of the system copies the selected text: Command-C on macOS, Control-C on Linux, and Control-C or Control-Insert on Windows. A line break separates the text of two `Text` elements. The copy has the text that shows, after `text-transform`.
- A press on content that is not selectable keeps the selection, as CSS UI 4 says. A press in a text control removes the selection, because the control has its own selection.
- A screen reader reads the selection: on the native targets, the `Text` has the text runs and the text range of AccessKit.
- The text of an `Input` and of a `TextArea` stays selectable on all targets, also with `select-none`. CSS UI 4 gives an editable element the used value `contain`. So `select-none` on a field has no effect, and `surf lint` gives the finding `field-select` for it. `selectOnFocus` selects the text of the field, also with `select-none` (component reference 4.7).
- The web writes `-webkit-user-select: none` and `user-select: none` as the default, on the element of the app. `select-text` writes `user-select: text` (section 10). `Input` and `TextArea` get `user-select: text` with `!important`, so that each engine keeps the selection of a field.
- The engines differ in the copy and in the keys of a selection, so the runtime of the web writes the copy and moves the moving end with Shift and an arrow key itself, in an app with `select-text` (`selectText` in `surf.js`).
- Only an app with `select-text` has the code of the selection. `select-none` alone links no code.

### 5.11 Paint of shapes (`Drawing`, ADR-0029)

| Property | Values | Default | Inh. | CSS | Native |
|---|---|---|---|---|---|
| `fill` | color, or `none` | the text color. A line and a polyline have no fill | yes | `fill` | coverage mask |
| `stroke` | color, or `none` | `none` | yes | `stroke` | coverage mask of the outline of the stroke |
| `stroke-width` | number of 0 or more, in view units | `1` | yes | `stroke-width` | |
| `stroke-dasharray` | lengths in view units, or `none` | no dashes | yes | `stroke-dasharray` | the dashes cut the outline first |
| `stroke-linecap` | `butt`, `round`, `square` | `butt` | yes | `stroke-linecap` | |
| `stroke-linejoin` | `miter`, `round`, `bevel` | `miter` | yes | `stroke-linejoin` | a miter longer than 4 stroke widths is a bevel |

The paint properties change no box. The fill rule is non-zero. A list of dashes with an odd count repeats, as in SVG.

### 5.12 Scroll snapping (`Scroll`, `List` and their children, ADR-0128)

| Property | Values | Default | Inh. | CSS | Native |
|---|---|---|---|---|---|
| `scroll-snap-type` | `none`, or an axis (`x`, `y`, `both`) and a strictness (`mandatory`, `proximity`) | `none` | no | `scroll-snap-type` | the scroll code of the runtime |
| `scroll-snap-align` | `none`, `start`, `center`, `end` | `none` | no | `scroll-snap-align` | a snap point of the scroll container |
| `scroll-snap-stop` | `normal`, `always` | `normal` | no | `scroll-snap-stop` | a snap point that a gesture does not pass |

The rules of CSS apply:

- `scroll-snap-type` goes on the scroll container. It has two parts: the axis and the strictness. The default strictness is `proximity`, as in CSS and in Tailwind 4.
- A child with `snap-start`, `snap-center` or `snap-end` is a snap point. At the snap point, the start edge, the center or the end edge of the child is at the same place in the visible area.
- With `mandatory`, the content always comes to rest at a snap point. With `proximity`, it comes to rest at a snap point only when one is near.
- CSS lets each browser choose the distance of `proximity`. WebKit and Firefox snap when a snap point is nearer than 0.3 of the size of the scrollport. macOS and Linux have no system setting for this distance. So the native targets use the same ratio (ADR-0128). There, a snap point is near when its distance is less than 0.3 of the size of the scroll container in that axis.
- `snap-none`, `snap-align-none` and `snap-normal` set the defaults again: no snapping, no snap point, and a snap point that a gesture can pass. A variant can turn a feature off in this way: `md:snap-none`.
- `snap-always` on a child stops a scroll gesture at that child, so the gesture does not pass it.
- On the web, the browser snaps, and the runtime snaps a wheel where the engines differ (see below). On the native targets, the runtime moves the content to the nearest snap point when a scroll gesture ends.
- A snap area is an element below the scroll container, also deeper than a child, whose nearest scroll container it is, as in CSS. A snap area that is larger than the visible area is valid at each position at which it covers that area. A snap area that does not show on the other axis is not a candidate. With `snap-both`, each axis snaps alone.
- After a layout pass, a scroll container goes back to the snap area at which it rested, as in CSS. Without such an area, a container with `snap-mandatory` goes to its nearest snap position.

On the native targets, the shell of each system tells the runtime when a scroll gesture ends. The table gives the end for each input, after the browsers. Then the content moves to its snap position in 250 ms, with the easing `ease-out-cubic`. With reduced motion, it moves at once.

| Input | End of the gesture | Snap position |
|---|---|---|
| A wheel with steps | Each step ends its gesture at once. | From the snap points ahead of the start in the direction of the step, the point nearest to the end of the step. This is the rule of CSS Scroll Snap 1 (section 6.2), and Chromium and Gecko use it. |
| A trackpad on macOS | The fingers leave the trackpad, or its momentum ends. The runtime waits 100 ms for momentum, as Chromium does. | The nearest snap point. |
| A touchpad on Linux (Wayland) | The event `axis_stop` of the compositor, when the fingers leave the touchpad. The shell has no momentum. The runtime waits 100 ms. | The nearest snap point. |
| A device without phases: a precise touchpad on Windows, a wheel without steps | 100 ms after the last scroll. | The nearest snap point. |
| The keys | Each key ends its gesture at once, as a step of a wheel. Not in version 0: on the native targets, a scroll of a key does not snap (ADR-0131). | The nearest snap point in the direction of the key. |
| A scroll bar | The native targets have no scroll bar. | |

A gesture does not pass a snap point with `snap-always`. Its start is the offset at the first scroll of the gesture. A scroll of the program, for example a scroll of the focus into view, ends no gesture. The next layout pass then snaps a container with `snap-mandatory`.

On the web, the browser snaps with the CSS of the stylesheet. The engines differ at a wheel event in pixels. The browser tests measured these results on 2026-11-06, and the source code of each engine gives the cause:

- Chromium snaps a step of a wheel in its direction. It takes the wheel event of the browser tests as a precise scroll, as from a trackpad, and snaps it to the nearest snap point.
- WebKit goes to the first snap point after the end of the step. On macOS, it first scrolls by the whole step, and it starts the snap 100 ms later. So a step can pass the snap point nearest to its end, and also a snap point with `snap-always`.
- Gecko before version 150 does not snap after a wheel event in pixels (Mozilla bug 2008571). The browser tests send each wheel event in pixels. A wheel in lines snaps in the direction of the step, with the rule of the table.

The web gives no phase of a wheel event, so the runtime cannot tell a step of a wheel from a move on a trackpad. An app with scroll snapping gets the snap of a wheel in the runtime. It acts only in these two cases. In the first case, a step and a move give the same snap point. In the second case, the browser did not snap:

1. A wheel event starts a gesture and passes the snap point of a step. This point is the first point with `snap-always` that the step passes, or the point nearest to its end. Then the runtime stops the scroll of the browser and moves the content to that point.
2. A gesture of one wheel event does not rest at a snap point 200 ms after its last scroll. Then the runtime moves the content to the snap point of a step. WebKit on macOS starts its own snap 100 ms after a step, so the runtime waits longer.

A wheel event 100 ms or less after the last one is a part of the same gesture, as on a trackpad. The content moves with the smooth scroll of the browser, and with reduced motion it moves at once. Each other gesture keeps the snap of the browser, also a trackpad and a touch. So a gesture of one wheel event that passes no snap point can still come to rest at another point in each engine (`docs/native-differences.md`).

---

## 6. Layout algorithms

- **Flex** follows CSS Flexible Box Layout Level 1, with the defaults of sections 3 and 5. `order` and `flex-flow` are not in v0. The reverse directions never come (ADR-0129). Baseline alignment uses the first baseline of each item.
- **Grid** follows CSS Grid Layout Level 1 for the features of sections 5.5 and 5.6.
- **Absolute** elements are placed after their parent's layout, from `inset`, `width` and `height`.
- **Leaves** (text, controls, images) give their size through a measure function (section 7).

The native runtime calls Taffy for one subtree and keeps the results of unchanged subtrees. A change of a style property marks only the element and its ancestors for layout (plan, section 8.1).

---

## 7. Text measurement

A `Text` element is a leaf. Its measure function answers three questions from the layout engine:

1. **Max-content width**: the width of the text without line breaks.
2. **Min-content width**: the width of the widest word.
3. **Height for a width**: the number of lines at that width, multiplied by the line height.

Rules:

- **Line height** `normal` is the ascent, plus the descent, plus the line gap of the font, from the `hhea` table of the font. A number multiplies the font size. A line height in px is not in v0, because a number without a unit is a length everywhere else (section 2).
- The text measure rounds each of the three metrics of `normal` to whole logical px before the addition, as WebKit and Chromium do (ADR-0131). A line box then has a height of whole px, so at the scale 1 each baseline is on a whole pixel.
- **Line breaks** are at break opportunities of the Unicode line breaking algorithm (UAX #14). This applies to all text on all targets, also to ASCII text (ADR-0131). So a line can end after a hyphen.
- **Tracking**: text has the tracking of the `trak` table of its font for its font size. The font size is also the optical size (`opsz`) of the font. A font without `trak` has no tracking. `letter-spacing` adds to this tracking (ADR-0131).
- **Baseline**: the first baseline is the ascent of the first line, plus half of the extra line height (`(line height − ascent − descent) / 2`), as in CSS.
- The web target uses the text engine of the browser, and the native target uses `surf-text`. The two engines can give different glyph widths for the same font. Section 11 explains how the conformance tests handle this.

### 7.1 A paragraph of several runs

A `Text` with a `Span` or a `Link` in it is one paragraph of several runs (ADR-0111). A run is the text of one element with its style. The native targets use these rules, which are the rules of inline boxes in CSS:

- The text of all runs is one text. The line breaks follow the rules above for the whole text, so a line can break inside a `Span`. A part of a run is not a break opportunity.
- The native targets shape the runs of one font together. A change of the color, the background or the line of a run does not change a width.
- Each line has the height of its inline boxes on one baseline: the `Text` itself, and each element with text on the line. An inline box is as high as its `line-height`. Half of the extra height is above its ascent, and half is below its descent. So a large `Span` makes its line taller, and a `leading-` of the `Text` also applies to its parts.
- A space at the end of a wrapped line hangs. It takes no width for the line and for the alignment, and it stays in the box of its element.
- The box of a part is the box of its text on each line. It goes from the ascent of its font above the baseline to the descent below the baseline. On more than one line, the box of the element holds all of them.
- The min-content width is the width of the widest word of the whole text.
- The background of a part paints behind the glyphs of its line.

---

## 8. Pixel snapping

- Layout works in logical px with `f32` values.
- Before painting, each edge is rounded to the nearest device pixel. The runtime rounds the absolute position of each edge, not the size, so that adjacent elements do not get gaps or overlaps. The width of an element is `round(x + width) − round(x)`.
- Taffy does this with its rounding mode. Browsers snap at paint time in the same way.

---

## 9. Paint order

- Elements paint in tree order: a parent before its children, and siblings in markup order.
- `z-index` changes the order only among siblings. A higher value paints later. Siblings with the same value keep markup order.
- The hit test uses the same order: the last element in paint order under the point gets it (ADR-0128).
- An element with `z-index` other than 0, with `opacity` less than 1, with a `transform`, with `sticky` (ADR-0128), with `fixed`, with a `filter` or with a mask is a **group**. An element with `isolate`, a `mix-blend-mode`, a `backdrop-filter` or a shape of a mask is a group too (ADR-0145). Its children never paint between other elements outside the group. CSS creates a stacking context in the same cases, because every element has `position: relative` (section 3).
- The native renderer draws a group with `opacity`, a `transform`, a `filter` or a `mix-blend-mode` into an offscreen layer, then draws the layer (plan, section 8.2). Another group paints into the layer around it, and the renderer keeps the place where its content starts. A blend mode and a backdrop read the content of the group from there (ADR-0145).

---

## 10. The web mapping

The web emitter writes one stylesheet for each app. The runtime adds no styles of its own. The stylesheet has four parts, in this order:

1. The base stylesheet below. The class `s-<Component>` comes from the name of the built-in component. `s-app` is the element that holds the app.
2. The theme tokens of section 2.1 as CSS variables on `:root`. The dark values are in `@media (prefers-color-scheme: dark)`, the high contrast values are in `@media (prefers-contrast: more)`, and all are also under `data-theme` on the root (ADR-0072, ADR-0103). A length token is a variable only when `Theme.setToken` can change it (ADR-0130).
3. The declarations of the utilities of each component. A component comes after the components that it uses, so the utilities of a parent win on the roots of a child. Each declaration becomes one class, `<Component>-<n>`. Equal declarations in one component share one class. An element gets the classes of the declarations of its utilities.
4. The declarations are in the order of section 12.3. A selector of the base stylesheet is inside `:where()`, so each utility wins over it. Each utility has the specificity of one class, also with a state, so the order alone gives the precedence.

```css
:where(.s-app) { font-family: var(--surf-font-sans, system-ui); font-size: var(--font-size); color: var(--text); }
:where(.s-Column, .s-Row, .s-Stack, .s-Grid, .s-Scroll, .s-List, .s-Text,
.s-Button, .s-Toggle, .s-Image, .s-Icon, .s-Input, .s-TextArea, .s-Slider,
.s-Select, .s-Canvas) {
  box-sizing: border-box;
  position: relative;
  margin: 0;
  padding: 0;
  border: 0 solid transparent;
  min-width: 0;
  font: inherit;
  color: inherit;
}
:where(.s-Column, .s-List, .s-Scroll) { display: flex; flex-direction: column; }
:where(.s-Row, .s-Button, .s-Toggle) { display: flex; flex-direction: row; }
:where(.s-Button) { align-items: center; justify-content: center; background: none; }
:where(.s-Toggle) { align-items: center; }
:where(.s-Scroll) { overflow: auto; }
:where(.s-Stack, .s-Grid) { display: grid; }
:where(.s-Stack > *) { grid-area: 1 / 1; }
:where(.s-Text) { display: block; white-space-collapse: preserve; }
:where(.s-Text .s-Link) { display: inline; unicode-bidi: normal; }
:where(.surf-fragment) { display: contents; }
:where(g.surf-fragment) { display: inline; }
```

- Each property of section 5 becomes one CSS property, with the value written in CSS syntax. A length becomes `px`. A color token becomes `var(--name)`. A length token becomes `var(--name)` when it can change, and else its value (ADR-0130).
- A logical side becomes the logical property of CSS, for example `padding-inline-start` (section 5.2, ADR-0124).
- `mask-fade-s-` and `mask-fade-e-` have no logical property in CSS. Their rule has a second selector with `:dir(rtl)`, which swaps the start edge and the end edge (ADR-0124). `:dir()` reads the attribute `dir` of HTML, so `dir-ltr` and `dir-rtl` also write this attribute on the element. A host page with `dir="rtl"` then also works, because `:dir()` follows the attribute of each ancestor.
- The attribute `dir` has the value of the declaration of `dir` that applies. `dir-auto` writes no attribute, so `:dir()` takes the direction of the parent. A condition changes the attribute when its value changes. The style of a component instance wins over the style of the root element of the component, also for the attribute. A `dir-` utility with a state, a theme or a window width changes the attribute when the variant changes, for example `hover:dir-rtl` or `md:dir-ltr`. The runtime reads the direction of the element from its rules. It does this in the next frame after a change of a state, of the window, of the theme or of the app. Only an app with such a utility has this code of the runtime. The native targets use the direction of the style of each element (section 5.9).
- The four shadow layers become one `box-shadow`. A gradient and a background image become the two layers of one `background-image`, the gradient first (section 5.8, ADR-0127).
- A text shadow becomes `text-shadow`. In an app with a color part of `text-shadow`, each layer of a size writes its color as `var(--surf-tsc, <color>)`, and the color part writes `--surf-tsc`. The stylesheet registers `--surf-tsc` with `@property` and `inherits: false`, as Tailwind 4 does. So the parts merge, and a color of a parent does not go to the size of a child (ADR-0145).
- A shape of `mask` becomes `clip-path`. In an app with a shape and another mask, a shape also writes `mask-image: none`, and another mask writes `clip-path: none`. So a variant can change a fade to a shape. `transition-mask` writes `clip-path` into the transition (ADR-0145).
- `backdrop-filter` also writes `-webkit-backdrop-filter`, for WebKit before Safari 18 (ADR-0145).
- A utility with a computed value (`w-{width}px`) becomes an inline style on the element. It wins over every class.
- An element where two variants or conditions set different parts of `shadow` or of `background-image` gets one class that writes the property from custom properties, as for `transform`. Each part is one custom property, for example `--surf-rw` for the width of the ring, so the parts merge (ADR-0120, ADR-0127). A stop without a color takes the stop before it, so it changes nothing.
- An element with a computed transform, or with variants or conditions on different parts of `transform`, gets one class with the rule `transform: translate(var(--surf-tx), var(--surf-ty)) scale(var(--surf-s)) rotate(var(--surf-r))`. Each part is one custom property, so the parts merge (ADR-0104). A class or an inline style sets one part. The stylesheet registers the four properties with `@property` and with `inherits: false`, so a change does not restyle the children.
- The states become pseudo-classes: `hover:` is `:hover`, `active:` is `:active`, `focus-visible:` is `:focus-visible` and `disabled:` is `:disabled`. A `Toggle` has the focus and the disabled state on its check box. `group-hover:` is `:where(.s-group:hover) .<class>`, so it keeps the specificity of one class.
- `dark:` and `light:` become `@media (prefers-color-scheme: ...)` and a selector for `data-theme` (ADR-0072). `max-600:` becomes `@media (width < 600px)`, and `min-600:` becomes `@media (width >= 600px)`. The compiler keeps a declaration with a platform variant only when the platform includes `web`.
- A condition (`{condition}:opacity-0.5`) adds the class of its declaration while the condition is true.
- Two variants or conditions can set different parts of one property on one element. Then each declaration also gets rules for its parts, after the rules of the whole properties (ADR-0120). A part is a CSS longhand, for example `padding-top`, `row-gap`, `border-left-width`, `border-color`, `transition-duration` or `transition-timing-function`. A part of `filter` is a custom property, as a part of `transform` is, and a rule of the whole `filter` then reads `grayscale(var(--surf-gs)) brightness(var(--surf-br)) blur(var(--surf-bl))`. The native targets apply the same parts in the same order.
- The nodes of a block have no wrapper: they go before a comment (ADR-0048, ADR-0121). So the elements of a block are children of the element around the block, as on the native targets. Four kinds of nodes have a wrapper with the class `surf-fragment`, which takes no part in the layout: the rows of a `List`, a `#try` block, a block in a `Drawing` (a `<g>`) and a `Transition`.
- `testid` becomes the attribute `data-testid`.
- The base stylesheet gives `:where(.s-app)` the rule `user-select: none`, and `:where(.s-Input, .s-TextArea)` the rule `user-select: text !important`. `select-text` becomes `user-select: text`. Each rule of `user-select` also has the name of WebKit, `-webkit-user-select` (section 5.10, ADR-0131).
- `Button` and `Input` keep their semantic HTML elements for accessibility. The base stylesheet removes the browser's own sizes from them, so that the layout is the same as on native.

---

## 11. Conformance

The conformance suite (plan, section 10) checks that the two implementations give the same boxes.

- A **fixture** is a small `.surf` component with a fixed window size. The `testid` of an element is its label in the results.
- The suite lays out each fixture in WebKit, Gecko and Chromium (through Playwright) and in the native runtime (Taffy). It records the border box of every element.
- Two results conform when each edge of each box differs by 0.5 logical px or less.
- Fixtures use a **test font with fixed metrics**, for example a font where each glyph is a square of 1 em. With this font, all engines measure text the same, and the suite tests the layout and not the text engines. On the web, `font: sans` and `font: mono` become `var(--surf-font-sans, system-ui)` and `var(--surf-font-mono, ui-monospace, monospace)`, and the page of the suite sets the two variables to the test font.
- Every property, every value and every default in section 5 needs at least one fixture. A new property needs fixtures before it becomes valid. The test `crates/surf-check/tests/layout_coverage.rs` expands the utilities of the fixtures and checks this rule.

---

## 12. Utilities

A **utility** is one word in the `style` attribute of an element. It sets one property of section 5, or one part of a property. The compiler knows every utility. A word that is not a utility is a compile error.

```surf
<Row style="items-center gap-3 px-4 py-2 border rounded hover:bg-$surface max-600:gap-1" />
```

### 12.1 Values

- For spacing and sizes, a number is a number of steps (ADR-0098). One step is `step` in `style.toml`, and the default is `0.25rem`. So `p-2` is `padding: 0.5rem`, which is 8 logical px at the default root size. A step can be a fraction: `p-0.5`, `p-2.5`.
- The scale applies to the forms of padding, margin, `gap-`, `inset-`, `top-`, `right-`, `bottom-`, `left-`, `inset-s-`, `inset-e-`, `translate-x-`, `translate-y-` and `indent-`. It also applies to `w-`, `h-`, `size-`, `min-w-`, `min-h-`, `max-w-`, `max-h-` and `basis-`.
- A value with a unit is not a number of steps: `p-2px` is 2 logical px, and `p-1rem` and `w-50%` keep their meaning. `0` stays `0`.
- Other utilities keep exact numbers: `border-2` is 2 logical px, `text-14` is 14 logical px, `rounded-8` is 8 logical px and `blur-4` is 4 logical px. `ring-2` is 2 logical px, and `bg-linear-45` is 45 degrees (ADR-0127).
- A theme token keeps its `$`: `bg-$surface`, `p-$space`. A token with no `$` is a compile error (ADR-0090). A name with no `$` is a constant of `style.toml` or a built-in name: `gap-inset`, `text-xs`. A constant is a length in logical px.
- A value that has spaces in section 5 has `_` for each space: `shadow-0_2_8_rgb(0_0_0_/_0.15)`, `grid-cols-1fr_200_auto`.
- The parentheses of a shape of `mask` hold the value of the CSS function, with `_` for each space: `mask-circle(40%_at_50%_0)`. A comma separates two points of a polygon, as in CSS. A number is a length in logical px, as in `rounded-8` (ADR-0145).
- The parentheses of `bg-url()` hold the whole path (ADR-0127). In them, a `:` is not the end of a variant, and `_` stays `_`, as in a URL of Tailwind 4: `bg-url(https://example.com/a_b.png)`.
- A `-` before a utility of a margin, an inset, `z-`, `translate-`, `rotate-`, `skew-`, `indent-` or `underline-offset-` makes its value negative: `-mt-1` is a top margin of one step up. A negative value cannot use a token. This is the one form: `translate-x--1` is a compile error (ADR-0104).
- A computed value goes in braces after the prefix: `w-{steps}`, `w-{width}px`, `w-{percent}%`, `text-{size}`. It follows the rule of a literal, so `w-{steps}` is a number of steps. It cannot have a variant or a condition. A number for `text-` is a `font-size`, and a `String` is a `color`. These prefixes take a computed value:
  `w-`, `h-`, `min-w-`, `min-h-`, `max-w-`, `max-h-`, `p-`, `m-`, `gap-`, `inset-`, `grow-`, `shrink-`, `basis-`, `z-`, `opacity-`, `bg-`, `rounded-`, `leading-`, `tracking-`, `line-clamp-`, `text-`, `fill-`, `stroke-`, `left-`, `top-`, `right-`, `bottom-`, `inset-s-`, `inset-e-`, `cursor-`, `translate-x-`, `translate-y-`, `scale-`, `rotate-`, `delay-`, `animate-delay-`.
- `inset-s-` and `inset-e-` take the same values as `left-` and `right-`, also a computed value: `inset-s-{x}px` (ADR-0124, ADR-0161).
- `bg-url({path})` takes a computed path, as the `src` of an `Image` does (ADR-0127). The path can have computed parts: `bg-url(assets/{name}.png)`.
- `translate-x-{x}` is a number of steps, `translate-x-{x}px` is a number of logical px, and `translate-x-{x}%` is a percentage of the box. `scale-{s}` is a factor. `rotate-{r}` is in degrees. `delay-{ms}` is in ms. A computed value of `delay-` takes the place of the delay of the `transition` of the element, so a row of a `#for` can read its index: `delay-{i * 40}`.
- A number of `scale-` and `brightness-` is a factor below 10, and a number of `leading-` is a factor below 4. Tailwind 4 reads such a number as a percentage or as a number of steps. So a larger number is a compile error that gives the form of Surface: `scale-95` gives `scale-0.95`, `brightness-50` gives `brightness-0.5`, and `leading-6` gives `leading-1.5` (ADR-0161). A number of `opacity-` or `grayscale-` above 1 is a compile error with the same kind of help: `opacity-50` gives `opacity-0.5`.
- An old name of ADR-0161 is a compile error. The help gives the new word with its value and its variants, for example `hover:grid-cols-3` for `hover:cols-3`.
- `animate-delay-{ms}` takes a computed value in ms, as `delay-{ms}` does (ADR-0150). It takes the place of the delay of the `animation` of the element.
- `view-transition-{name}` takes a computed `String` (ADR-0154). A computed text that is not a name of CSS gives no name, on all targets.

### 12.2 Variants

A variant comes before a utility and ends with `:`. A utility can have one state and one condition of the table: `dark:hover:bg-$surface`. `{value}:` is also a variant (12.5). `(...)` groups utilities under the same variants: `hover:(underline opacity-0.8)`.

| Variant | The declaration applies |
|---|---|
| `hover:`, `active:`, `focus-visible:`, `disabled:` | in this state of the element. `active:` applies while a press holds the element, and `focus-visible:` while the element has the keyboard focus and shows its focus ring |
| `focus-within:` | while the element or one of its descendants has the keyboard focus, also without a focus ring, as `:focus-within` of CSS (ADR-0160) |
| `group-hover:`, `group-active:`, `group-focus-visible:`, `group-disabled:`, `group-focus-within:` | while an ancestor with the utility `group` has the state (ADR-0106, ADR-0160) |
| `peer-hover:`, `peer-active:`, `peer-focus-visible:`, `peer-disabled:`, `peer-focus-within:` | while an earlier sibling with the utility `peer` has the state, as the selector `~` of CSS (ADR-0160). The element needs such a sibling in the same list of children: a block has a list of its own. Else the variant is a compile error |
| `motion-reduce:` | while the setting "reduce motion" of the system is on, as `prefers-reduced-motion: reduce` (ADR-0160) |
| `rtl:`, `ltr:` | when the element has this direction, as `:dir()` of CSS (ADR-0160). They cannot change `dir` |
| `forced-colors:` | while the system chooses the colors, as `forced-colors: active` of CSS (ADR-0051). Only a browser has this mode, so a native target never applies it |
| `dark:`, `light:` | when the app shows this theme: the theme of the system, or the mode of `Theme.setMode`. A high contrast mode counts as its appearance. |
| `macos:`, `windows:`, `linux:`, `android:`, `ios:`, `web:`, `apple:`, `mobile:`, `desktop:`, `native:` | on these targets. The groups are the groups of language spec, section 14. |
| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | when the window is as wide as the breakpoint, or wider. The defaults are 640, 768, 1024, 1280 and 1536 logical px. `[breakpoint]` in `style.toml` can change a value and add a name (ADR-0097). |
| `max-sm:`, `max-md:`, `max-lg:`, `max-xl:`, `max-2xl:` | when the window is less wide than the breakpoint |
| `min-<n>:` | when the window is `<n>` logical px wide or wider |
| `max-<n>:` | when the window is less than `<n>` logical px wide |

### 12.3 Parts and precedence

- Some properties have parts. The utilities of one element give one declaration for each property. These are the parts:
  - the four sides of `padding`, `margin` and `inset`,
  - the two axes of `gap`,
  - the width and the color of `border`,
  - the functions of `transform` and of `filter`,
  - the properties, the duration, the easing and the delay of `transition`,
  - the two logical sides of `padding`, `margin`, `inset` and `border-width`, and the four logical corners of `radius` (ADR-0124),
  - the kind of the gradient of `background-image`, the color and the position of each stop, and the picture (ADR-0127),
  - the size and the color of each layer of `shadow` (ADR-0127),
  - the size and the color of `text-shadow` (ADR-0145),
  - the axis and the strictness of `scroll-snap-type` (ADR-0128). `snap-mandatory` and `snap-proximity` need an axis: `snap-x`, `snap-y` or `snap-both`. Without one, they are a compile error.

  A variant takes the parts of `background-image` and of `shadow` that it does not set from the utilities without a variant.
- A utility of one side wins over a utility of an axis. A utility of an axis wins over a utility of all sides. So `p-2 pt-0` gives `padding: 0 8 8 8`.
- A physical side wins over a logical side, as in the stylesheet of Tailwind 4 (ADR-0124). So `ps-2 pl-4` gives a left padding of 16 in a left-to-right element. A logical side wins over an axis, so `px-2 ps-4` gives a start padding of 16.
- These ranks count only inside one variant. A variant always wins over the base, also when the base sets a physical side and the variant sets a logical side (ADR-0124). So `pl-1 hover:ps-4` gives 16 px on the left side on hover, from left to right, as in Tailwind 4. Between two different variants, the order at the end of this section decides. A condition is a variant for this rule (section 12.2).
- A utility of all sides or of the axis `x` also sets the two logical sides, as `padding` and `padding-inline` do in CSS. So `ps-4 hover:p-0` gives a start padding of 0 on hover. In the same way, `rounded-` sets the four logical corners.
- For `radius`, a utility of one corner wins over a utility of a side, and a utility of a side wins over `rounded-`. For example, `rounded-8 rounded-s-0 rounded-ss-4` gives these radii: 4 at the top start corner, 0 at the bottom start corner, and 8 at the two end corners.
- Two utilities of one element with the same variants that set the same part are an error, for example `p-2 p-4`. So the order of the words has no effect.
- A utility with a variant takes the parts that it does not set from the utilities without a variant: `p-2 hover:pt-0` gives `padding: 0 8 8 8` on hover.
- A utility with a condition also takes these parts: `transition {open}:duration-300` gives the transition of `transition` with 300 ms while `open` is true. It does not take the parts of another condition, so `{open}:transition {open}:duration-300` is a compile error.
- The parts of a property merge when two declarations both apply. `{a}:pt-2 {b}:pb-3` gives the top padding of `a` and the bottom padding of `b` while `a` and `b` are both true, also when `a` and `b` are one condition (ADR-0120). `{a}:translate-x-5 {b}:rotate-20` moves and turns the element (ADR-0104). On hover while `a` is true, `border {a}:border-3 hover:border-$accent` gives a border of 3 px in the color of the accent. A part that neither declaration sets comes from the utilities without a variant, or it is the default. Two declarations that set the same part follow the order below. The six properties of `transition` are one part, so `{a}:transition-opacity {b}:transition-transform` gives the properties of one declaration.
- A computed function of `transform` merges too: `translate-x-{x}px hover:scale-1.1` keeps the value of `x` on hover.
- `animation` is one declaration, because the time and the easing of an animation come from its name.
- `animate-delay-`, `animate-timeline-` and `animate-range-` change the one declaration of `animation`, as `animate-duration-` does (ADR-0150, ADR-0151). On the time, a range has no effect, as in CSS: `hover:animate-timeline-auto` keeps the range of the element. A range on an element with no `animate-timeline-` utility is a compile error.
- Two declarations of one element can set the same property or the same part, and both apply. Then this order gives the winner, from the lowest to the highest:
  1. no variant
  2. a platform
  3. `min-<n>:`, where a larger `<n>` wins
  4. `max-<n>:`, where a smaller `<n>` wins
  5. a direction: `rtl:`, `ltr:` (ADR-0160)
  6. a theme
  7. `motion-reduce:` (ADR-0160)
  8. `forced-colors:`
  9. a state, with or without a condition
- A condition wins over the utilities of `style` with the same variants.
- The `style` of a component instance applies to the root elements of the component, with its computed values and its conditions.

### 12.4 Reference

| Utility | Property | Value |
|---|---|---|
| `w-<v>`, `h-<v>` | `width`, `height` | a length, `<n>%` or `auto` |
| `min-w-<v>`, `min-h-<v>` | `min-width`, `min-height` | a length or `<n>%` |
| `max-w-<v>`, `max-h-<v>` | `max-width`, `max-height` | a length, `<n>%` or `none`. A name of `max-w-` is a width of `container` or a length of `spacing` (section 2.1) |
| `aspect-<n>`, `aspect-<a>/<b>`, `aspect-auto` | `aspect-ratio` | `auto`: no ratio (ADR-0160) |
| `p-`, `px-`, `py-`, `pt-`, `pr-`, `pb-`, `pl-` | `padding`: all sides, an axis, one side | a length |
| `ps-`, `pe-` | `padding`: the start side, the end side (ADR-0124) | a length |
| `m-`, `mx-`, `my-`, `mt-`, `mr-`, `mb-`, `ml-` | `margin` | a length or `auto` |
| `ms-`, `me-` | `margin`: the start side, the end side (ADR-0124) | a length or `auto` |
| `gap-`, `gap-x-`, `gap-y-` | `gap`: both axes, the column gap, the row gap | a length |
| `items-<v>` | `align` | `start`, `center`, `end`, `stretch`, `baseline` |
| `justify-<v>` | `justify` | `start`, `center`, `end`, `between`, `around`, `evenly` |
| `flex-wrap`, `flex-nowrap` | `wrap` | |
| `content-<v>` | `align-content` | `start`, `center`, `end`, `stretch`, `between`, `around` |
| `grow`, `grow-<n>`, `shrink`, `shrink-<n>` | `grow`, `shrink` | without a value: 1 |
| `basis-<v>` | `basis` | a length, `<n>%` or `auto` |
| `self-<v>` | `align-self` | the values of section 5.4 |
| `grid-cols-<n>`, `grid-rows-<n>` | `columns`, `rows` | `<n>` equal tracks: `repeat(<n>, 1fr)` |
| `grid-cols-<tracks>`, `grid-rows-<tracks>` | `columns`, `rows` | a track list: `grid-cols-1fr_200_auto`. One track of a length is `grid-cols-repeat(1,_200)`. A name of a line is in brackets: `grid-cols-[side]_200_[main]_1fr`. `repeat()` takes `auto-fill` and `auto-fit`: `grid-cols-repeat(auto-fill,_minmax(200,_1fr))` (ADR-0126) |
| `grid-areas-<rows>` | `areas` | the names of the cells. `_` separates two cells, and `/` separates two rows: `grid-areas-head_head/nav_main` (ADR-0126) |
| `auto-rows-<track>` | `auto-rows` | one track size |
| `auto-cols-<track>` | `auto-columns` | one track size: `auto-cols-40`, `auto-cols-minmax(0,_1fr)` (ADR-0160) |
| `grid-flow-row`, `grid-flow-col` | `auto-flow` | `row`, `column`. These names replace `flow-row` and `flow-col` (ADR-0126) |
| `grid-flow-dense`, `grid-flow-row-dense`, `grid-flow-col-dense` | `auto-flow` | `dense`, `row dense`, `column dense` (ADR-0126) |
| `justify-items-<v>`, `justify-self-<v>` | `justify-items`, `justify-self` | the values of sections 5.5 and 5.6 |
| `col-<v>`, `row-<v>` | `column`, `row` | a placement: `col-2`, `col-1_/_3`. A line can be a name: `col-main` (ADR-0126). A line can be `<n>_<name>`, for example `col-2_main`, and a span can be `span_<name>`, for example `col-1_/_span_main` |
| `area-<name>` | `area` | the name of an area of the parent: `area-nav` (ADR-0126). A name that the parent in the same file does not have is a compile error |
| `col-span-<n>`, `row-span-<n>` | `column`, `row` | `span <n>` |
| `col-span-<name>`, `row-span-<name>` | `column`, `row` | `span <name>`: to the next line with the name, for example `col-span-main` (ADR-0126) |
| `relative`, `absolute`, `sticky`, `fixed` | `position` | `sticky` stays in the visible area of its scroll container, and `fixed` needs a place on both axes (section 5.7) |
| `group` | `group` | a parent for `group-hover:`. It takes no variant |
| `flex-row`, `flex-col` | `direction` | `row`, `column`. An element keeps its own direction without one |
| `hidden` | `display` | `none`. `max-767:hidden` removes the element from the layout on a phone |
| `pointer-events-none`, `pointer-events-auto` | `pointer-events` | `none`: the pointer goes to the element below. `auto`: a child takes the pointer again. Children get the value (ADR-0160) |
| `visible`, `invisible` | `visibility` | `visible`, `hidden`: the element keeps its box and paints nothing of its own. Children get the value (ADR-0160) |
| `peer` | `peer` | an earlier sibling for `peer-<state>:`. It takes no variant (ADR-0160) |
| `inset-`, `inset-x-`, `inset-y-`, `top-`, `right-`, `bottom-`, `left-` | `inset` | a length, `<n>%` or `auto` |
| `inset-s-`, `inset-e-` | `inset`: the start side, the end side (ADR-0124, ADR-0161) | the values of `left-` and `right-`: a length, `<n>%` or `auto`, also a computed value (12.1) |
| `z-<n>` | `z-index` | an integer |
| `overflow-hidden`, `overflow-visible` | `overflow` | `hidden`, `visible` |
| `object-fill`, `object-contain`, `object-cover`, `object-none` | `object-fit` | how the picture of an `Image` fills its box |
| `object-center`, `object-top`, `object-bottom`, `object-left`, `object-right`, `object-top-left`, `object-top-right`, `object-bottom-left`, `object-bottom-right` | `object-position` | the place of the picture in its box. The positions are physical, as in Tailwind 4.1 (ADR-0160) |
| `mask-none`, `mask-ellipse` | `mask` | no mask, or the ellipse in the box. A mask limits the element and its children. The hit area stays the box (ADR-0065) |
| `mask-fade-t-<n>`, `-r-`, `-b-`, `-l-`, `-x-`, `-y-`, `-s-`, `-e-` | `mask` | the element fades to nothing over `<n>` at that edge, or at both edges of `x` and `y`. `<n>` is a length, a length of `spacing` or a length token. `-s-` is the start edge, and `-e-` is the end edge (ADR-0124). On the web, their rule uses `:dir(rtl)` (section 10) |
| `mask-inset(<sides>)`, `mask-inset(<sides>_round_<radii>)` | `mask` | a rectangle inside the border box, with round corners: `mask-inset(0_50%_0_0)`, `mask-inset(8_round_12)`. The sides are as `padding`, and the radii as `radius` (section 5.7, ADR-0145) |
| `mask-circle(<radius>)`, `mask-circle(<radius>_at_<x>_<y>)` | `mask` | a circle: `mask-circle(40%)`, `mask-circle(closest-side_at_left_top)` (section 5.7, ADR-0145) |
| `mask-ellipse(<rx>_<ry>)`, `mask-ellipse(<rx>_<ry>_at_<x>_<y>)` | `mask` | an ellipse: `mask-ellipse(50%_30%_at_50%_0)` (section 5.7, ADR-0145) |
| `mask-polygon(<x>_<y>,<x>_<y>,...)` | `mask` | a polygon with 3 or more points: `mask-polygon(50%_0,100%_100%,0_100%)` (section 5.7, ADR-0145) |
| `bg-<color>` | `background` | a color |
| `bg-linear-to-<side>`, `bg-linear-<n>`, `bg-radial`, `bg-conic`, `bg-conic-<n>` | `background-image`: the kind of the gradient | `<side>` is `t`, `tr`, `r`, `br`, `b`, `bl`, `l` or `tl`. The sides are physical, as in Tailwind 4 (ADR-0124). `<n>` is an angle in degrees (ADR-0127) |
| `from-<v>`, `via-<v>`, `to-<v>` | `background-image`: a stop of the gradient | a color or a position `<n>%`: `from-$accent`, `from-10%` (ADR-0127) |
| `via-none` | `background-image`: the `via` stop | no `via` stop: the gradient has only `from` and `to` (ADR-0127) |
| `bg-none` | `background-image`: the kind of the gradient and the picture | `none`: no gradient and no picture (ADR-0127) |
| `bg-url(<path>)` | `background-image`: the picture | a path in the build or a URL, as the `src` of an `Image` (ADR-0127) |
| `bg-auto`, `bg-cover`, `bg-contain` | `background-size` | `auto`, `cover`, `contain` (ADR-0127) |
| `bg-center`, `bg-top`, `bg-bottom`, `bg-left`, `bg-right`, `bg-top-left`, `bg-top-right`, `bg-bottom-left`, `bg-bottom-right` | `background-position` | `center`, a side or a corner. `bg-top-left` is the default. The positions are physical, as in Tailwind 4 (ADR-0127) |
| `bg-repeat`, `bg-no-repeat` | `background-repeat` | `repeat`, `no-repeat` (ADR-0127) |
| `border` | `border` | `1 $border` |
| `border-<n>`, `border-<color>` | `border`: the width, the color | the other part is `1` or `$border` |
| `border-t`, `border-r`, `border-b`, `border-l`, and `-<n>` after them | `border-width` | one side. Other sides are 0 until a utility sets them |
| `border-s`, `border-e`, and `-<n>` after them | `border-width` | the start side, the end side (ADR-0124) |
| `rounded`, `rounded-<v>` | `radius` | without a value: `$radius`. Else 1 to 4 lengths. |
| `rounded-s`, `rounded-e`, and `-<v>` after them | `radius`: the two corners of the start side, of the end side (ADR-0124) | without a value: `$radius`. Else a length |
| `rounded-ss`, `rounded-se`, `rounded-es`, `rounded-ee`, and `-<v>` after them | `radius`: one corner. The first letter is the top side (`s`) or the bottom side (`e`). The second letter is the start side or the end side (ADR-0124) | without a value: `$radius`. Else a length |
| `shadow-<v>` | `shadow`: the shadow layer | `<dx>_<dy>_<blur>_<color>` or `none`. A size of `style.toml`: `shadow-xs`, `shadow-sm`, `shadow-md`, `shadow-lg`, `shadow-xl` (section 2.1). A spread: `<dx>_<dy>_<blur>_<spread>_<color>`. A color: `shadow-$border-strong` (ADR-0127) |
| `inset-shadow-<v>` | `shadow`: the inset shadow layer | as `shadow-`. The sizes are `2xs`, `xs` and `sm` (ADR-0127) |
| `ring`, `ring-<n>`, `ring-<color>` | `shadow`: the ring layer | a width or a color. Without a value: 1 (ADR-0127) |
| `inset-ring`, `inset-ring-<n>`, `inset-ring-<color>` | `shadow`: the inset ring layer | as `ring` (ADR-0127) |
| `opacity-<n>` | `opacity` | a number from 0 to 1 |
| `skew-x-<n>`, `skew-y-<n>` | `transform` | the angle of a skew in degrees, after the other functions, as in Tailwind 4. A `-` before the utility gives a negative angle (ADR-0160) |
| `origin-center`, `origin-top`, `origin-bottom`, `origin-left`, `origin-right`, `origin-top-left`, `origin-top-right`, `origin-bottom-left`, `origin-bottom-right` | `transform-origin` | the point that a transform keeps. The positions are physical (ADR-0160) |
| `translate-x-<v>`, `translate-y-<v>`, `scale-<n>`, `rotate-<n>` | `transform` | in this order: translate, scale, rotate. `scale-<n>` is a factor below 10 (12.1). `rotate-<n>` is in degrees. Each takes a computed value (12.1). `translate-x-` is physical, as in Tailwind 4: a positive value moves the element to the right, also in `rtl` (ADR-0124). |
| `grayscale-<n>`, `brightness-<n>`, `blur-<v>` | `filter` | in this order. `grayscale-` is a number from 0 to 1, and `brightness-` a number from 0 to below 10 (section 12.1). `blur-` is a length: `blur-4` is 4 logical px. All targets (section 5.8, ADR-0131) |
| `backdrop-blur-<v>` | `backdrop-filter` | a blur of the content behind the element: `backdrop-blur-8` has a standard deviation of 8 logical px. `backdrop-blur-0` is `none` (section 5.8, ADR-0145) |
| `mix-blend-<mode>` | `mix-blend-mode` | a blend mode of section 5.8: `mix-blend-multiply`, `mix-blend-plus-lighter`. `mix-blend-normal` is the default (ADR-0145) |
| `isolate`, `isolation-auto` | `isolation` | `isolate`, `auto` (section 5.8, ADR-0145) |
| `font-sans`, `font-mono`, `font-<name>` | `font` | `<name>` is a font of the theme (section 2.1) |
| `text-<n>` | `font-size` | a length |
| `font-<n>` | `font-weight` | an integer from 100 to 900 |
| `leading-<v>` | `line-height` | `normal`, or a number below 4 (section 12.1) |
| `text-start`, `text-center`, `text-end` | `text-align` | |
| `text-wrap`, `text-nowrap` | `text-wrap` | `wrap`, `none` |
| `dir-auto`, `dir-ltr`, `dir-rtl` | `dir` | `auto`, `ltr`, `rtl`. On the web, `dir-ltr` and `dir-rtl` also write the attribute `dir` (section 10, ADR-0124) |
| `italic`, `not-italic` | `font-style` | `italic`, `normal` |
| `underline`, `line-through`, `no-underline` | `text-decoration` | `underline`, `line-through`, `none` |
| `uppercase`, `lowercase`, `capitalize`, `normal-case` | `text-transform` | `uppercase`, `lowercase`, `capitalize`, `none` |
| `wrap-anywhere`, `wrap-normal` | `overflow-wrap` | `anywhere`, `normal` |
| `tabular-nums`, `normal-nums` | `font-variant-numeric` | `tabular-nums`, `normal` |
| `font-stretch-<v>` | `font-stretch` | a word of CSS, for example `font-stretch-condensed`, or a percentage: `font-stretch-75%` (ADR-0160) |
| `decoration-<n>` | `text-decoration-thickness` | logical px (ADR-0160) |
| `underline-offset-<n>` | `text-underline-offset` | logical px. A `-` before the utility gives a negative offset (ADR-0160) |
| `indent-<v>` | `text-indent` | a length: a number of steps, as `p-` (ADR-0098). A `-` before the utility gives a negative indent. Not in version 0: the native targets (ADR-0160) |
| `caret-<color>`, `accent-<color>`, `placeholder-<color>` | `caret-color`, `accent-color`, `placeholder-color` | a color (ADR-0160) |
| `tracking-N` | `letter-spacing` | `N` |
| `line-clamp-<v>` | `max-lines` | an integer or `none` |
| `field-sizing-content` | `field-sizing` | `content` |
| `text-<color>` | `color` | a color |
| `text-shadow-<v>` | `text-shadow`: the size | a size of `style.toml`: `2xs`, `xs`, `sm`, `md` or `lg`, with the values of Tailwind 4. One layer: `text-shadow-0_1_2_#00000080`. Or `none` (section 5.9, ADR-0145) |
| `text-shadow-<color>` | `text-shadow`: the color | the color of each layer of the size: `text-shadow-$accent` (ADR-0145) |
| `fill-<color>`, `fill-none` | `fill` | a color or `none` |
| `stroke-<color>`, `stroke-none` | `stroke` | a color or `none` |
| `stroke-<n>` | `stroke-width` | a number in view units |
| `stroke-dash-<a>_<b>...`, `stroke-dash-none` | `stroke-dasharray` | lengths in view units |
| `stroke-cap-butt`, `stroke-cap-round`, `stroke-cap-square` | `stroke-linecap` | |
| `stroke-join-miter`, `stroke-join-round`, `stroke-join-bevel` | `stroke-linejoin` | |
| `snap-x`, `snap-y`, `snap-both` | `scroll-snap-type`: the axis | on a `Scroll` or a `List` (ADR-0128) |
| `snap-none` | `scroll-snap-type` | `none`: no snapping (ADR-0128) |
| `snap-mandatory`, `snap-proximity` | `scroll-snap-type`: the strictness | without one: `proximity` (ADR-0128). They need an axis |
| `snap-start`, `snap-center`, `snap-end`, `snap-align-none` | `scroll-snap-align` | on a child of the scroll container. `snap-align-none`: no snap point (ADR-0128) |
| `snap-always`, `snap-normal` | `scroll-snap-stop` | `always`, `normal` (ADR-0128) |
| `cursor-<name>` | `cursor` | the pointer over the element: a cursor of CSS of section 5.10. Each target shows the cursor of its system, or the nearest cursor that the system has (ADR-0160, `docs/native-differences.md`) |
| `select-none`, `select-text` | `user-select` | `none`, `text`. Text is not selectable without `select-text`. `select-text` makes the text of the element and of its children selectable. The text of an `Input` and a `TextArea` stays selectable, also with `select-none`, and `surf lint` reports `select-none` on a field (rule `field-select`, section 5.10, ADR-0131). `select-auto` of version 0 is a compile error that names `select-text` |
| `transition` | `transition` | all six properties of section 5.10 |
| `transition-<property>` | `transition` | one property: `opacity`, `transform`, `background`, `color`, `shadow`, `border` (its color), `mask` (its shape, ADR-0145) |
| `duration-<n>` | `transition` | the duration in ms. Without it: 150. |
| `delay-<n>` | `transition` | the delay in ms. Without it: 0 (ADR-0104). |
| `size-<value>` | `width`, `height` | both sizes (ADR-0053). |
| `text-<name>` | `font-size`, `line-height` | a text size of the theme. A `leading-` of the element wins (ADR-0053). |
| `sr-only`, `not-sr-only` | `position`, `width`, `height`, `overflow`, `opacity` | hides the box and keeps the element for a screen reader, and undoes this (ADR-0051). |
| `outline-none` | `outline` | no focus ring, on all targets. An app that draws its own focus style with `focus-visible:` needs it (ADR-0131) |
| `animate-<name>`, `animate-none` | `animation` | a looping animation: `pulse`, `spin`, `ping`, `bounce`, or a name of `keyframes` in `style.toml` (ADR-0049). |
| `animate-duration-<n>`, `animate-ease-<easing>`, `animate-count-<n>`, `animate-infinite` | `animation` | the time of a round in ms, the easing and the number of rounds. Without them: the values of the animation, without an end. |
| `animate-delay-<n>` | `animation`: the delay | the time in ms before the first round. Without it: 0. It takes a computed value: `animate-delay-{i * 150}` (section 5.10, ADR-0150) |
| `animate-timeline-scroll`, `animate-timeline-scroll-x` | `animation-timeline` | the scroll position of the scroll container of the element, on the axis `y` or `x` (section 5.10, ADR-0151) |
| `animate-timeline-view`, `animate-timeline-view-x` | `animation-timeline` | the place of the element in the visible area of its scroll container (ADR-0151) |
| `animate-timeline-<name>`, `animate-timeline-auto` | `animation-timeline` | a timeline that `scroll-timeline-<name>` or `view-timeline-<name>` names in the same file, or the time (ADR-0151) |
| `animate-range-<v>` | `animation-range` | the start and the end on the timeline: `animate-range-entry`, `animate-range-entry_25%_cover_50%`, `animate-range-0_400` (section 5.10, ADR-0151) |
| `scroll-timeline-<name>`, `scroll-timeline-x-<name>` | `scroll-timeline` | names the scroll position of a `Scroll` or a `List`, on the axis `y` or `x`. It takes no variant (ADR-0151) |
| `view-timeline-<name>`, `view-timeline-x-<name>` | `view-timeline` | names the place of the element in the visible area of its scroll container, on the axis `y` or `x`. It takes no variant (ADR-0151) |
| `cue-<name>` | `cue` | the element plays the cue `<name>` of a sequence of its component (language spec 7.5a, ADR-0152). It takes no variant |
| `split-lines`, `split-words`, `split-letters` | `split` | on a `Text` or a `Span`: the motion utilities of the element apply to each line, word or letter (section 5.10, ADR-0153). It takes no variant |
| `split-clip` | `split` | each part of a split text paints only inside its own box (section 5.10, ADR-0153). It takes no variant |
| `view-transition-<name>` | `view-transition-name` | the element moves from the old page to the new page in a page transition. It takes a computed `String`: `view-transition-{name}` (wave spec 7, ADR-0154) |
| `ease`, `ease-in`, `ease-out`, `ease-in-out`, `ease-linear`, `ease-cubic-bezier(<a>,<b>,<c>,<d>)` | `transition` | the easing. Without it: `ease`. |
| `ease-in-<name>`, `ease-out-<name>`, `ease-in-out-<name>` | `transition` | a named curve of section 5.10: `sine`, `quad`, `cubic`, `quart`, `quint`, `expo`, `circ`, `back` (ADR-0104). |
| `ease-spring`, `ease-spring-<bounce>` | `transition` | a spring of section 5.10. `animate-ease-` and the `ease` of a `Transition` take the same names (ADR-0104). |

A color is a token (`$accent`), `#rgb`, `#rrggbb`, `#rrggbbaa`, `transparent` or `rgb(<r>_<g>_<b>_/_<a>)`.

### 12.5 Conditions and case lists

- `{condition}:` is a variant (ADR-0096): `{saved}:bg-$success`, `{!open}:max-md:hidden`. The value must be a `Bool`.
- `{value}:{ case: utilities, }` gives one style for each case of an enum:

  ```surf
  <Button style="
      px-4
      {variant}:{
          primary: bg-$accent text-$on-accent,
          ghost: text-$text-secondary,
          plain:,
      }
  ">Save</Button>
  ```

- The list must name each case. A case with no style is `name:,`. A case with fields is a compile error.
- A case can hold utilities, variants, groups, presets and conditions. A variant can come first: `hover:{variant}:{ ... }`.
- The value of a case list must be an enum. A `String` value uses one condition for each value.
- A case list cannot have a condition, and a utility with a condition cannot have a computed value.

---

## 13. Decisions for version 1

This section listed the open questions for version 1. On 2026-11-04, the project owner decided them. Version 1 has these features:

1. Logical sides next to the physical sides: `ps-`, `pe-`, `ms-`, `me-`, `inset-s-`, `inset-e-`, `border-s-`, `border-e-` and the logical corners of `rounded-` (5.2, ADR-0124).
2. Percentages keep the rules of version 0. `padding`, `margin` and `gap` never take a percentage (2, ADR-0125).
3. Named lines, areas, `auto-fill`, `auto-fit` and dense in a grid (5.5, 5.6, ADR-0126).
4. Linear, radial and conic gradients, four shadow layers with a spread, and background images (5.8, ADR-0127).
5. `sticky` on the native targets, and scroll snapping (5.7, 5.12, ADR-0128).
6. No reverse flex directions, also not later (5.3, ADR-0129).
7. Theme lengths that `Theme.setToken` changes while the app runs (2.1, ADR-0130).

On 2026-11-05, the project owner answered the open details of these features:

1. `dir-ltr` and `dir-rtl` also write the attribute `dir` of HTML. The web rule of `mask-fade-s-` and `mask-fade-e-` uses `:dir(rtl)`, so a host page with `dir="rtl"` also works (5.9, 10, 12.4, ADR-0124).
2. A variant always wins over the base. The rank of a physical side over a logical side counts only inside one variant (12.3, ADR-0124).
3. An `area-<name>` with a name that the parent in the same file does not have is a compile error (5.6, ADR-0126).
4. The placements `span <name>` and `<n> <name>` of CSS: `col-span-main` and `col-2_main` (5.6, 12.4, ADR-0126).

These features come after version 1:

- Percentages in `calc()`, in a minor version (ADR-0125).
- Subgrid, when Taffy supports it (ADR-0126).
