Surf
Surface Layout Spec, v0
The layout and style model: units, the box model, style properties, utilities and theme tokens.This page as MarkdownStatusdraft
Editionnone yet. Version 0 has no stability guarantee.
Related documentsLanguage spec v0 (section 12), component reference, ADR-0005, PLAN.md (section 10)
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.Rules for values: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).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).A gap value of one length applies to both axes.Logical sides (ADR-0124).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().The utility grow without a value means grow: 1.A track list is a list of track sizes, separated by spaces. A track size is one of these: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: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):Rules for visual properties:Text shadows (ADR-0145).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):The paint properties change no box. The fill rule is non-zero. A list of dashes with an odd count repeats, as in SVG.The rules of CSS apply: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:A color is a token ($accent), #rgb, #rrggbb, #rrggbbaa, transparent or rgb(<r>_<g>_<b>_/_<a>).
- 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.
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)
- 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.
toml
step = "0.25rem"[color]accent = ["#9f40ff", "#d0bcff"]tertiary = ["#7d5260", "#efb8c8"]outline = "#79747e"[length]radius = 12space-huge = 32[spacing]inset = 12[font]sans = "assets/Inter.ttf"display = "assets/Display.ttf"[breakpoint]md = 700[scale]design = [460, 674]min = 0.75max = 2.5[keyframes.fade]ms = 500ease = "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
- 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.
- 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).
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.
- 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.
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
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)
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
- 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.
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
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)
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 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)
- 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.
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
- 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.
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)
- 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 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).
- 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.
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
- 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 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.
- 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.
- 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.
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-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.
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
- 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.
- 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).
- 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.
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
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 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:
- 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.
- 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.
surf
<Text style="text-48 font-700 split-letters animate-rise">One universal best option</Text>
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
- 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.
- 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.
- 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).
- 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 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.
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
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
- 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.
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.
- 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.
- 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.
- 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.
- 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).
- Max-content width: the width of the text without line breaks.
- Min-content width: the width of the widest word.
- Height for a width: the number of lines at that width, multiplied by the line height.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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).
- 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.
- 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.
- 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.
surfRun
<Row style="items-center gap-3 px-4 py-2 border rounded hover:bg-$surface max-600:gap-1" />
- 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.
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
- 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:
- no variant
- a platform
- min-<n>:, where a larger <n> wins
- max-<n>:, where a smaller <n> wins
- a direction: rtl:, ltr: (ADR-0160)
- a theme
- motion-reduce: (ADR-0160)
- forced-colors:
- 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.
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).
- {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.
- 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).
- Percentages keep the rules of version 0. padding, margin and gap never take a percentage (2, ADR-0125).
- Named lines, areas, auto-fill, auto-fit and dense in a grid (5.5, 5.6, ADR-0126).
- Linear, radial and conic gradients, four shadow layers with a spread, and background images (5.8, ADR-0127).
- sticky on the native targets, and scroll snapping (5.7, 5.12, ADR-0128).
- No reverse flex directions, also not later (5.3, ADR-0129).
- Theme lengths that Theme.setToken changes while the app runs (2.1, ADR-0130).
- 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).
- 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).
- An area-<name> with a name that the parent in the same file does not have is a compile error (5.6, ADR-0126).
- The placements span <name> and <n> <name> of CSS: col-span-main and col-2_main (5.6, 12.4, ADR-0126).
- Percentages in calc(), in a minor version (ADR-0125).
- Subgrid, when Taffy supports it (ADR-0126).