# Surface component reference, v0

- **Status:** draft
- **Edition:** none yet. Version 0 has no stability guarantee.
- **Related documents:** [Language spec v0](https://surface.solenovis.com/docs/language.md) (sections 9.2 and 9.6), [Surface Layout Spec v0](https://surface.solenovis.com/docs/layout.md), [GLOSSARY.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/GLOSSARY.md)
- **Changes:** 2026-11-06: `focusable={true}` and a computed `value` work on the native targets too (1, 3.2, ADR-0131). 2026-11-06: `Video`, `Track` and the bar of `controls` of a `Video` with its full screen work on all targets (4.5b to 4.5d, ADR-0155 to ADR-0158). The keys move the position `Slider` of the bar by 5 seconds, and its value stays exact (4.5d). 2026-11-06: the names of the styles of ADR-0161 in the samples and the text (4.2, 4.11, 4.13, 4.14). 2026-11-06: the events `resize` and `contextmenu`, `load` and `error` of `Image`, `target` of `Link`, `id` as a hook for an extern module, and the utilities of `inset` in `exit` of a `Transition` (1, 3, 3.1, 4.5, 4.5a, 4.15, ADR-0160). 2026-11-06: `srcset` and `loading` of `Image` work on all targets (4.5, ADR-0159). `Audio` and `Source` work on all targets, without the bar of `controls` and the media controls of the system (4.5b, 4.5c, ADR-0155, ADR-0156, ADR-0158). 2026-11-06: `bg-url()` paints a background picture on all targets (4.5, ADR-0127). 2026-11-05: the selection of a `Text` on all targets, and the text of a field stays selectable with `select-none` (4.4, 4.7, ADR-0131). On the native targets, a press that gives the focus to a field with `selectOnFocus` keeps the selection of its text, as in a browser (4.7). 2026-11-05: the alpha of the icon of `mask` counts, and the animated pictures play on the native targets (1, 4.5, ADR-0131). 2026-11-05: `Video`, `Audio`, `Source`, `Track` and the bar of `controls` (4.5b to 4.5d, ADR-0155 to ADR-0158), and `srcset` and `loading` of `Image` (4.5, ADR-0159). The spec marks them "Not in version 0". The answers of 2026-11-05 give five bindings, the full screen and the media controls of the system (ADR-0156). 2026-11-05: scroll snapping and `sticky` in a `Scroll` and a `List` on all targets (4.2, 4.3, ADR-0128). 2026-11-05: `text-nowrap` on a `Span` (4.4a). 2026-11-05: a `Link` to an address outside the app opens the browser of the system on the native targets (4.5a, W8 of `docs/plan/windows.md`). 2026-11-05: `ease` of `Transition` takes only the name of an easing utility, as `ease` of `motion` (4.15, ADR-0104). 2026-11-05: Enter that ends the composition of an input method gives no `change` and no `submit` (3.2). All targets have `change` at Enter. 2026-11-05: `change` of an `Input` also comes at Enter after an edit, before `submit`, as in a browser (3, 3.2, 4.7, ADR-0113). The spec marks this "Not in version 0". 2026-11-04: `side` of `Overlay` and of `Tooltip` takes `start` and `end` on all targets (4.14, 5.12, ADR-0124). 2026-11-04: `select-none` on an `Input` or a `TextArea` turns off the selection of the field, and `surf lint` reports it (4.7, ADR-0131). The spec marks this "Not in version 0". 2026-11-04: one behavior on all targets for `focusable={true}` and a `value` that the program computes again (1, 3.2, ADR-0131). Text is not selectable without `select-text`, and the text of an `Input` and a `TextArea` stays selectable (4.4, 4.7, ADR-0131). An animated GIF, an animated WebP and an APNG play on all targets (4.5, ADR-0131). `side` of `Overlay` and of `Tooltip` takes `start` and `end` (4.14, 5.12, ADR-0124). The spec marks these features "Not in version 0". 2026-11-04: scroll snapping and `sticky` in a `Scroll` and a `List` (4.2, 4.3, ADR-0128), and the paths of `bg-url()` (4.5, ADR-0127). The spec marks these features "Not in version 0". 2026-11-03: a control with the focus that gets `hidden` loses the focus on each target, `rtl` of `Tabs`, and `ToastList` (3.2, 5.9, 5.14, 5.15, ADR-0122). 2026-11-03: a shape and a `Group` take only their paint, the opacity, a text color and the cursor (4.11, ADR-0120). 2026-11-03: a computed value and a case list on a `Span`, and `focusable={true}` on an element that is not a control (1, 4.4a, ADR-0118). 2026-11-03: an `Input` has one line, a text control keeps the text of the user, and `autofocus` of a `Select` (4.7, 4.13, ADR-0113). 2026-11-03: `invalid`, a key in an overlay is `typing`, a `hidden` element is out of the accessibility tree and the order of the Tab key, `level` of `Card`, `alt` and `tabStop` of `Menu`, and `restart` and `action` of `Toast` (1, 3, 4.3, 5, ADR-0110). 2026-11-03: `Span`, and a `Link` in a `Text` (4.4, 4.4a, 4.5a, ADR-0111). 2026-11-03: the components of `surf:ui` (5, ADR-0102), `focus` and `passive` for `Overlay`, and the focus and the keys on containers. 2026-11-03: `scroll`, `visible`, `pointerenter` and `pointerleave` (3, 3.1, 4.2, ADR-0106). 2026-10-30: `Transition` (4.15, ADR-0036). 2026-10-28: `style` takes utilities, `class` is removed, `testid` is new, and the style properties are no longer attributes. `Box` and the `variant` and `tone` of `Text` are removed (ADR-0020). 2026-10-26: first version (P1.8). 2026-10-26: `autofocus` for `Input` and `TextArea` (P1.14). 2026-10-27: `alt` for all components (P2.8).

This document gives the attributes, the bindings and the events of each built-in component. The compiler checks each attribute and each event on a built-in component against this document. An attribute or an event that is not in this document is a compile error. The checker takes the same table from `crates/surf-check/src/builtins.rs`.

---

## 1. Attributes of all components

Each built-in component takes these attributes in addition to the attributes in its own table:

- `style`: the utilities of the component (layout spec, section 12), for example `style="p-2 gap-1"`. A computed value of a utility must be an `Int`, a `Float` or a `String`.
- `id` (`String`): the name of the element for the relations of a screen reader (`controls`, `labelledBy`, `describedBy`, `activeDescendant`) and for `Focus.to`. It is also the hook of an extern module: on the web, the element has the attribute `id`, so the web file of the module finds it with `document.getElementById` (ADR-0160). A fixed `id` must be unique (`surf lint`, rule `duplicate-id`).
- `role` (a role in quotes), `level`, `datetime`, `expanded`, `pressed`, `selected`, `checked`, `decorative`, `focusable`, `current`, `controls`, `activeDescendant`, `autocomplete`, `keyShortcuts`, `labelledBy`, `describedBy`, `invalid`, `hasPopup`, `orientation`, `valueNow`, `valueMin`, `valueMax`, `valueText`: the meaning of the component for a screen reader (ADR-0051, ADR-0102, ADR-0110). `controls`, `labelledBy`, `describedBy` and `activeDescendant` name another element by its `id`. `focusable={false}` leaves a control out of the order of the Tab key. `focusable={true}` on an element that is not a control makes it a stop of the Tab key on all targets, and it gets the keys (ADR-0131). A `tabpanel` gets the focus in the same way. A `Scroll` with the focus scrolls with the keys of a browser: the arrow keys, Page Up, Page Down, Home, End and the space bar. `invalid` (`Bool?`) marks a field with an error: `aria-invalid` on the web, and the invalid state of AccessKit on the native targets. On the native targets, the text of the element of `describedBy` is also the description of the control, because not each adapter of AccessKit reads the relation.
- `alt` (`String`): the name of the component for a screen reader. It replaces the text of the component, for example `<Button alt="Delete {todo.title}">×</Button>`. A container with `alt` is a group with this name. On the web, `alt` is the `alt` of an `Image` and the `aria-label` of each other component.
- `title` (`String`): a text for a pointer that rests on the component, and its description for a screen reader (ADR-0032). On the web, it is the `title` attribute. On the native targets, a small label shows after 0.5 seconds.
- `lang` (`String`): the language of the text of the component and of its children, as a tag such as `ja` or `zh-Hans` (ADR-0066). A Han character has different forms in Chinese and Japanese. On the web, it is the `lang` attribute. On the native targets, it chooses the font and the forms of the shaper. The first 8 bytes of a tag count, as whole subtags. Without it, the platform uses the languages of the user.
- `mask` (`Icon`): limits the component and its children to the shape of an icon (ADR-0065). The shape is the largest that fits into the box, in its middle. The alpha of the icon counts, as in `mask-image` of CSS: a shape with an opacity of 0.4 shows 40 % of the element (ADR-0131). The hit area stays the box. The shapes of a `Drawing` do not take it.
- `testid`: a name for tests, in quotes, without values. It has no effect on the app. On the web, it is the attribute `data-testid`. The test driver of the desktop runtime finds elements by it.

A style property is not an attribute. `gap={12}` is a compile error, and its help text gives the utility `style="gap-3"`.

---

## 2. Props

A prop of a built-in component has one of these types:

| Type | Values |
|---|---|
| `String` | Any text. A string attribute can insert values: `placeholder="New {kind}"`. |
| `Bool` | `true` or `false`. The attribute name alone means `true`: `disabled`. |
| `Float` | A `Float` value. An `Int` is a compile error, as everywhere in the language: write `min={0.0}`. |
| `List<Float>` | The points of a `Polyline` or a `Polygon`: x and y, one after the other. |
| `Icon` | An icon: a constant from an `.svg` import, or a value of `Icon.parse` (section 4.12). |

A prop without a value has the default that the table gives. A bindable prop can take `bind:` (language spec, section 9.2). Each component has at most one bindable prop, except `Video` and `Audio`, which have five (section 4.5b, ADR-0156).

---

## 3. Events

An event handler is a function name, statements, or a lambda whose body is statements (language spec, section 9.2). Some events have a value. A handler can take the value as a parameter or ignore it:

```surf
<Input on:keydown={key => {
    if key == "Escape" {
        editing = false;
    }
}} />
<Input on:submit={add} />
<Button on:press={count += 1}>Add</Button>
```

| Event | Value | When |
|---|---|---|
| `press` | none | The user activates a `Button`: a click, a tap, or Enter or Space while the button has the focus. |
| `doubleclick` | none | Two clicks in a short time on the component. All built-in components have it, shapes too. A `Button` also gets two `press` events before the `doubleclick`. |
| `focus` | none | The control gets the keyboard focus. |
| `blur` | none | The control loses the keyboard focus. |
| `key` | `press: KeyPress` | As `keydown`, with `key`, `shift`, `ctrl`, `alt` and `meta` (ADR-0052). `on key(press, typing)` in a component gets each key press of the window (ADR-0075). `typing` is true while the focus is in an `Input`, a `TextArea`, a `Select` or an `Overlay`, so that a key of a menu or a dialog does not start a shortcut of the app (ADR-0110). |
| `keydown` | `key: String` | A key goes down while the control has the focus. The value is the key name of the W3C UI Events specification, for example `"Enter"`, `"Escape"`, `"ArrowUp"` or `"a"`. |
| `input` | the new value | Each edit of the value: each change of the text, or each step of a slider while the user drags it. |
| `change` | the new value | The user commits a value. For `Input`: Enter after an edit, or the loss of the focus after an edit. For `TextArea`: the loss of the focus after an edit. For `Toggle`: each change. For `Slider`: the user releases it. |
| `submit` | none | Enter in an `Input`. After an edit, `change` comes first. Enter does not send `submit` while an input method composes text. |
| `scroll` | `position: ScrollPosition` | A `Scroll` or a `List` scrolled (ADR-0106). The record has `x` and `y`, how far the content moved, and `maxX` and `maxY`, the largest values, in logical px. The event does not go to a parent. |
| `visible` | `visible: Bool` | A part of the element enters the window (`true`), or its last part leaves the window (`false`). A clip of an ancestor counts. Another element on top does not count. All built-in components with a box have it (ADR-0106). An element that starts outside the window sends nothing. |
| `resize` | `size: BoxSize` | The border box of the element has a new size (ADR-0160). The built-in record `BoxSize` has `width` and `height` in logical px, before the transforms, as `ResizeObserver` with the border box. The size before the first event is 0 × 0, so an element with no area sends nothing at the start. A `hidden` element has the size 0 × 0. All built-in components with a box have it. |

### 3.1 Pointer events

Every built-in component has the pointer events (P3.2). Their value is the built-in record `Pointer`:

```surf
type Pointer = {
    x: Float,      // the position in the border box of the component, in logical px
    y: Float,
    windowX: Float, // the position in the window, in px of the window: for a drag of a component that moves
    windowY: Float,
    dx: Float,     // the distance of a wheel in logical px: positive to the right and down
    dy: Float,
    button: Int,   // 0 main, 1 middle, 2 secondary; -1 for a move and a wheel
    shift: Bool,
    alt: Bool,
    ctrl: Bool,
    meta: Bool,    // Command on macOS, the Windows key on Windows
}
```

| Event | When |
|---|---|
| `pointerdown` | A button goes down on the component. The component captures the pointer: it gets the `pointermove` and the `pointerup` events until that button goes up, also when the pointer leaves its box. The press starts no text selection, no native drag and no context menu of the browser. |
| `pointermove` | The pointer moves: to the component with the capture, else to the component under the pointer. |
| `pointerup` | The button goes up: to the component with the capture, which then ends. A drag always ends with one `pointerup`: also after a lost capture, a context menu or the loss of the window focus. |
| `wheel` | A wheel or a scroll gesture over the component. The component takes it: no `Scroll`, `List` or page under it scrolls. |
| `pointerenter` | The pointer enters the component or one of its descendants (ADR-0106). The event does not go to a parent, and a parent gets its own event. |
| `pointerleave` | The pointer leaves the component and its descendants. A component that left the tree gets no event. |
| `contextmenu` | The user asks for a context menu (ADR-0160): a right click, at the press on macOS and Linux and at the release on Windows, as the system does. On macOS, Control and a click of the main button also ask for it, and that click is no press. The menu key and Shift with F10 ask for the menu of the component with the focus on each system: `x` and `y` are then the middle of the component, and `button` is -1. The menu of the browser does not open for a component that listens to the event. |

- The nearest component from the target up that listens to the event gets it, as for the other events. `focus`, `blur`, `keydown` and `key` work in the same way, so a container gets them from a control in it (ADR-0102). `pointerenter` and `pointerleave` go only to their component.
- A drag of a knob: `on:pointerdown` keeps the start, `on:pointermove` changes the value by `p.y`, and `p.shift` makes the steps smaller.
- On the web, a wheel in lines or pages becomes pixels, and a cancel of the browser (`pointercancel`) is a `pointerup`.
- `press` and `doubleclick` still come after a click, also on a component with pointer events.
- A `Drawing` and its shapes get `x` and `y` in the coordinates of the view of the drawing (section 4.11), not in the border box.

### 3.2 Rules of all events

These rules apply to all events:

- An element with `hidden` and its children get no pointer events, are out of the order of the Tab key, and are not in the accessibility tree, as in a browser (ADR-0110). A control with the focus that gets `hidden` loses the focus after the next style pass, as in a browser. It gets `change` after an edit, then `blur`, and no control has the focus. The Tab key goes on from its place (ADR-0122).
- On the native targets, a control that gets the focus from a key or from `Focus.to` scrolls into the area of each `Scroll` and `List` around it, as in a browser. It has the focus ring of the web target: 2 px of `$focus`, 1 px outside its box (ADR-0110).

- A component gets only the events in its table, and the pointer events.
- A control with `disabled={true}` gets no events.
- A binding writes the new value before the `on:` handlers of the same event run. The `bind:value` of `Input`, `TextArea` and `Slider` writes on `input`. The `bind:checked` of `Toggle` writes on `change`. The bindings of `Video` and `Audio` write on their events (section 4.5b).
- A change from the program does not send events. For example, a write to the bound place of an `Input` does not send `input`.
- `change` of a text control comes once for one edit, on all targets, as in a browser (ADR-0113). Enter in an `Input` after an edit gives `change` and then `submit`. The loss of the focus after that gives no second `change`. Enter with no edit gives only `submit`. Enter in a `TextArea` makes a new line, and it gives `input`, not `change`.
- Enter that ends the composition of an input method gives no `change` and no `submit`, as in a browser. The text of the composition stays, and a later Enter gives `change` and `submit`.
- A computed `value` that gives the same result again writes nothing (ADR-0131). So a text control keeps the text of the user when an unrelated source of the value changes.
- The runtime does not send an event to a component while a batch changes the tree. For example, the removal of a control that has the focus does not send `blur`.

---

## 4. Components

### 4.1 Containers: `Row`, `Column`, `Stack`, `Grid`

| Attribute | Type | Default |
|---|---|---|
| (none) | | |

Events: `doubleclick`, and `focus`, `blur`, `keydown` and `key` of the controls in the container (ADR-0102).

### 4.2 `Scroll`

No attributes. The event `scroll` gives the position (section 3).

- A `Scroll` takes the utilities of scroll snapping: `snap-x`, `snap-y` or `snap-both`, with `snap-mandatory` or `snap-proximity`. A child takes `snap-start`, `snap-center`, `snap-end` and `snap-always` (layout spec, section 5.12, ADR-0128).
- A `sticky` element stays in the visible area of its nearest `Scroll`, `List` or element with `overflow-hidden` (layout spec, section 5.7, ADR-0128).
- A screen reader can scroll each element into view, as in a browser. Each `Scroll` and `List` around the element then scrolls, also for a disabled control (ADR-0142).

### 4.3 `List`

`List` takes `items` and `key`, and the fill `#fill row(item) { ... }`. Language spec, section 7.7, gives the rules of the `row` slot, and section 9.5 gives the rules of keys. `List` has no events of its own.

| Attribute | Type | Default |
|---|---|---|
| `rowHeight` | `Float` | none |

A `List` is a scroll area on its main axis, as `Scroll` is. Only the rows near its visible area exist: the rows that touch the area, and one area height above and below it (ADR-0027).

- The runtime measures each row that it builds. A row that was never built has the average height of the first measured rows. With `rowHeight`, each row has that height, and the runtime does not measure. The rows must then have this height.
- A `List` needs a bounded height to build fewer rows, for example `grow` in a `Column` with a height, or `h-100`. A container between the `List` and that `Column` needs `min-h-0` (layout spec, section 3). A `List` without a bounded height grows to the height of all rows, so all rows exist.
- A row that leaves the window is destroyed, with its state, its effects and its tasks. A row that comes back is a new row.
- The rows do not shrink: a row has `shrink` 0 unless its style says else.
- The `List` has the role "list". Each row is in a container with the role "list item", the number of items and the position of its item. A row keeps its own role.
- A `#for` block builds all rows. Use `List` for a long list.
- A `List` takes the utilities of scroll snapping and holds `sticky` elements, as a `Scroll` does (section 4.2). Only the rows near the visible area exist, so a snap area of a row that does not exist is no snap point.
- A `List` in a window builds the rows of a tall window before its first layout pass, not all rows. A `List` in a `hidden` element keeps its rows until it shows again (ADR-0110).

### 4.4 `Text`

No attributes of its own. Events: `doubleclick`.

`Text` accepts text (language spec, section 9.3). The web element is `<span>`. Utilities give the size, the weight and the color: `style="text-20 font-600 text-$error"`.

A `Text` takes text, values, blocks, `Span` and `Link` (ADR-0111). Its parts are one paragraph:

```surf
<Text style="text-16">
    Run <Span style="font-mono bg-$surface">surf dev</Span>, then read the <Link href="/docs">documents</Link>.
</Text>
```

- A line can break inside a part. The line height comes from the largest part on the line.
- A screen reader gets the whole text of the `Text`, and a node for each `Link` in it.
- A `Text` is not selectable, unless it or an ancestor has `select-text` (layout spec, section 5.10, ADR-0131). With `select-text`, a drag selects text, also across the parts of the `Text`, a double click selects a word, and the copy key copies it.

### 4.4a `Span`

A part of a `Text` with a style of its own (ADR-0111). The web element is `<span>`.

- A `Span` is valid only in a `Text` or in another `Span`.
- Attributes: `title`, `lang` and `id`, with `style` and `testid`. A `Span` has no events.
- Its utilities are the utilities of text: color (`text-$accent`), font weight (`font-600`) and font family (`font-mono`). Size (`text-14`, `text-sm`), italic, underline, line-through, background color (`bg-$surface`) and `text-nowrap` are utilities of text too. With `text-nowrap`, a line breaks only before or after the `Span`, not inside it, so a short code sample such as `a-b` stays on one line. A state (`hover:`), a condition (`dark:`, `{value}:`), a case list and a computed value (`text-{color}`, `text-{size}`) work. Each other utility is a compile error.
- Blocks and values work in a `Span`: `#for t in tokens { <Span style="...">{t.text}</Span> }` writes a code sample from a list of tokens.

### 4.5 `Image`

| Attribute | Type | Default |
|---|---|---|
| `src` | `String` | none |
| `srcset` | `String`: a file for each pixel density | none |
| `loading` | `String`: `eager` or `lazy` | `eager` |

Events: `load` (no value) and `error` (`error: MediaError`) (ADR-0160). `alt` (section 1) is the text of the image for a screen reader.

- `load` comes when the file arrived and the picture has its size. `error` comes when the file did not load. Each file of an element sends one event, so a new `src` sends again. A lazy image sends after its load starts.
- The case of `MediaError` (section 4.5b) is `network` for a file that did not arrive: a file that is not in the build, an HTTP error, or no connection. It is `decode` for data with the signature of a format that each target shows (PNG, JPEG, GIF, WebP and SVG), and `format` for other data. On the web, the runtime reads the file again after an error to find the case.

- `src` is a path in the build or a URL. A file in `src/assets/` of a project goes into each build, and its path is `assets/logo.png` (ADR-0033). The web build copies the folder next to the page, and the desktop build next to the binary, or into `Contents/Resources` of a macOS app bundle.
- An `Image` has the size of its picture, one pixel of the file for each logical px. With a width or a height from the styles, the other side comes from the ratio of the picture. With both, the picture stretches to the content box, unless `object-contain`, `object-cover` or `object-none` says otherwise.
- The native targets show PNG, JPEG, WebP, GIF and SVG. An animated GIF, an animated WebP and an animated PNG (APNG) play on all targets. They play with the delays of their frames and with their count of loops. A delay of 10 ms or less is 100 ms, as in the browsers (ADR-0131). Windows shows the first frame of an animated WebP, because its WebP codec gives no frames (`docs/native-differences.md`). A JPEG comes upright, as its EXIF orientation says. A file that does not load paints nothing there. An image from an `http` or `https` address loads in the background and has no size until it arrives (ADR-0037). An SVG file has the size of its view and the shapes of an `Icon`. A path in quotes under `assets/` must name a file, or the compiler gives an error.
- `bg-url()` takes the same paths and URLs for the background of each element, and the same formats and rules of the load. A literal path under `assets/` must name a file (layout spec, section 5.8, ADR-0127). A background image is decoration with no alt text. Use an `Image` with `alt` for a picture with a meaning.

**A file for each pixel density** (ADR-0159).

```surf
<Image src="assets/logo.png" srcset="assets/logo.png 1x, assets/logo@2x.png 2x" alt="Surface" />
```

- `srcset` has the form of HTML with densities only: candidates with a comma between them. A candidate is a path or a URL, a space and a density with `x`, for example `2x` or `1.5x`. If `srcset` has no `1x` candidate, `src` is the `1x` candidate.
- A width with `w` and the attribute `sizes` are a compile error.
- The size of a file in logical px is its size in pixels, divided by its density. So a file of 200 × 100 pixels with `2x` has 100 × 50 logical px, and each candidate gives the same layout.
- A native target chooses the smallest density that is equal to or larger than the scale of the window. The scale is the count of device pixels for each logical px. If no candidate is that large, the largest density wins. A move to a screen with another scale, or a new scale of the UI, chooses again.
- On the web, the browser chooses the candidate, as HTML allows. It can take a smaller file, for example to save data (see [native differences](https://surface.solenovis.com/docs/targets.md)).
- In a literal `srcset`, each path under `assets/` must name a file. The syntax must be valid, and two candidates must not have one density. A computed `srcset` gets these checks at run time, and the runtime skips a candidate with an error.

**Lazy images** (ADR-0159).

- With `loading="lazy"`, the image starts its load when its box comes near the visible area of its nearest `Scroll`, `List` or window. With `eager`, it loads at once.
- On the native targets, "near" is one height and one width of that area, as the rows that a `List` builds (section 4.3). On the web, the browser chooses the distance, because HTML gives a page no way to set it.
- A `hidden` image with `lazy` does not load. A `hidden` image with `eager` loads, as in HTML.
- Before its load, an image without a size from its styles has no size. Give it a width and a ratio, for example `w-full aspect-16/9`, so that the layout does not move.

### 4.5a `Link`

A link (ADR-0051): an `<a>` on the web target, and a `Button` with the role of a link on the native targets.

Props: `href` (`String`), `target` (`String`), `alt`. `href` takes a relative URL, `http`, `https`, `mailto` and `tel`. For another scheme, the web target writes no attribute (ADR-0109).

`target="_blank"` opens the address outside the app (ADR-0160): in a new tab of the browser on the web, and in the browser of the system on the native targets. `target` takes only `"_blank"`. A literal address of the app with `target`, for example `href="/docs"`, is a compile error, because a native app has one window. A computed address of the app goes to the router of SurfWave on the native targets, as without `target`.

Events: `press`, `doubleclick`, `focus`, `blur`, `keydown`. `Link` accepts text.

With a handler for `press`, a press stays in the app. A press with a modifier key opens the address as the browser does. Without a handler, the browser follows the link. On a native target, a press without a handler opens an address with the scheme `http`, `https`, `mailto` or `tel` in the browser of the system. The system opens `mailto` and `tel` with the app for them. A relative URL goes to the router of SurfWave, and a link with another scheme does nothing.

A `Link` in a `Text` is a part of the text (ADR-0111). It keeps the focus, the keys, the hover state, the router and the check of its URL. It takes the utilities of a `Span` and `cursor-`. A `Link` in a `Link` is a compile error.

### 4.5b `Video` and `Audio`

A video and a sound (ADR-0155, ADR-0156). The web elements are `<video>` and `<audio>`. Not in version 0: a video that `autoplay` started does not pause while no part of it is in the window.

```surf
<Video poster="assets/intro.jpg" controls alt="Introduction" style="w-full rounded-8" bind:paused={paused} bind:currentTime={time}>
    <Source src="assets/intro.mp4" type="video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"" />
    <Source src="assets/intro.webm" type="video/webm; codecs=\"vp9, opus\"" />
    <Track src="assets/intro.en.vtt" kind="captions" srclang="en" label="English" default />
</Video>

<Audio src="assets/chime.mp3" controls alt="Chime" />
```

| Attribute | Type | Default |
|---|---|---|
| `src` | `String` | none |
| `poster` | `String` (only `Video`) | none |
| `autoplay` | `Bool` | `false` |
| `loop` | `Bool` | `false` |
| `controls` | `Bool` | `false` |
| `preload` | `String`: `none`, `metadata` or `auto` | `metadata` |
| `paused` | `Bool`, bindable | `true` |
| `currentTime` | `Float`, bindable: the time in seconds | `0.0` |
| `volume` | `Float`, bindable: from 0 to 1 | `1.0` |
| `muted` | `Bool`, bindable | `false` |
| `playbackRate` | `Float`, bindable: from 0.25 to 4 | `1.0` |
| `metadata` | `MediaMetadata` | the title from `alt`, the picture from `poster` |
| `labels` | `MediaLabels` | English texts |

Events: `play`, `pause`, `ended`, `timeupdate` (`time: Float`), `loadedmetadata` (`info: MediaInfo`), `volumechange` (`volume: MediaVolume`), `ratechange` (`rate: Float`), `nexttrack`, `previoustrack` and `error` (`error: MediaError`). With `controls`, also `focus`, `blur` and `keydown`.

```surf
type MediaInfo = { duration: Float, width: Float, height: Float }
type MediaVolume = { volume: Float, muted: Bool }
type MediaMetadata = { title: String = "", artist: String = "", album: String = "", artwork: String = "" }
enum MediaError { network, decode, format }
```

**Sources.**

- `src` is a path in the build or a URL with `http` or `https`, as the `src` of an `Image` (section 4.5). Version 1 plays a file. HLS, DASH and live streams are not in version 1.
- A list of `Source` elements gives more files (section 4.5c). `src` and a `Source` child on one element are a compile error.
- The element tries the sources in their order, as HTML does. It skips a source whose `type` the system cannot play, and it tries the next source after an error. After the last source, it sends `error` with `format`.
- Each target plays the formats of its media system, and ADR-0155 has the table. The media systems are AVFoundation on macOS, Media Foundation on Windows, GStreamer on Linux and the browser on the web. For a video, give an MP4 with H.264 and AAC first, then a WebM with VP9 and Opus. Each target then plays one of them. For a sound, an MP3 plays on each target.
- `surf lint` gives the finding `media-format` when the sources do not cover each target of the table. For a literal path under `assets/`, the compiler reads the format from the file.
- A target with no media system sends `error` with `format`, for example Linux without GStreamer. The poster stays.
- The `.deb` of `surf package` for an app with media recommends `gstreamer1.0-libav`, so that Ubuntu plays H.264 and AAC (ADR-0155).

**Events.**

| Event | When |
|---|---|
| `play` | The element starts to play, also after a pause. |
| `pause` | The element pauses: a press, the media controls of the system, or the end without `loop`. |
| `ended` | The play reached the end without `loop`. |
| `timeupdate` | Each 250 ms while the element plays, and after a seek of the user, a pause and the end. The value is the time in seconds. The rate is the same on all targets. |
| `loadedmetadata` | The media system read the duration and the size of the file. `width` and `height` are in logical px, and they are 0 for a file without video. |
| `volumechange` | The user changed the volume or `muted`. |
| `ratechange` | The user changed the rate through the media controls of the system. |
| `nexttrack`, `previoustrack` | The user pressed "next" or "previous" in the media controls of the system. |
| `error` | No source plays (`format`), the network failed (`network`), or the file has an error (`decode`). |

`Image` gets `on:load` and `on:error` with the same `MediaError` in P6.22.

**Bindings** (ADR-0156).

- A `Video` and an `Audio` have five bindable props: `paused`, `currentTime`, `volume`, `muted` and `playbackRate`. Each other built-in component has at most one (section 2). The rules of `bind:` of language spec 7.4 and 9.2 apply.
- The element writes the bound place at its events: `paused` at `play` and `pause`, `currentTime` at each `timeupdate`, `volume` and `muted` at `volumechange`, and `playbackRate` at `ratechange`. The write comes before the `on:` handlers of the event (section 3.2).
- A write of the program changes the media. `paused = false` plays, under the rule of sound below, and `paused = true` pauses. A new `currentTime` seeks to that exact time. A new `volume`, `muted` or `playbackRate` applies at once.
- A change of the program and a change of the media do not make a loop. The element keeps the last value of each prop: the value that it wrote, or the value that it took from the program. A new value that is equal to the kept value does nothing. Only a different value changes the media. So the write at each `timeupdate` never comes back as a seek.
- A change of the program sends no event (section 3.2). The next events come from the play.
- A value outside its range applies as the nearest value of the range. The range of `currentTime` is from 0 to the duration, of `volume` from 0 to 1, and of `playbackRate` from 0.25 to 4. The pitch of the sound stays at each rate.
- Before the metadata arrives, the element keeps a new `currentTime`, and it seeks when the metadata arrives.
- If the rule of sound refuses a play, the element stays paused. It writes `true` to the bound `paused` again at the end of the batch, with no event.
- Without `bind:`, a prop such as `muted={quiet}` sets the element. A change of the user stays until the program gives a new value, as the text of an `Input` (section 4.7).
- The duration and the size are read-only. They come from `loadedmetadata`.

**Size and paint.**

- A `Video` has the size of its video, one pixel of the file for each logical px, as an `Image`. Before the metadata arrives, it has the size of its poster, or else 300 × 150 logical px. A width or a height from the styles keeps the ratio.
- The picture fits into the box with `object-contain` by default. `object-cover`, `object-fill` and `object-none` work, as on an `Image`. The free area shows the background of the element.
- A frame paints as each other element: with the clip, the radius, the opacity, the transform and the mask of its element. Another element can paint over it.
- The poster shows until the first frame of the play, or until a seek. Without a poster, the first frame shows when `preload` loaded it.
- An HDR video shows in SDR on the native targets (see [native differences](https://surface.solenovis.com/docs/targets.md)).
- An `Audio` without `controls` has no box and is not in the accessibility tree, as in HTML. With `controls`, its box is the bar of the controls: 300 logical px wide by default.

**Autoplay, sound and motion** (ADR-0158).

- `autoplay` starts the element when its media is ready.
- With the setting "reduce motion" on, `autoplay` does not start a `Video`. The poster or the first frame shows. When the setting comes on, a video that `autoplay` started pauses. A video that the user started plays on.
- A video that `autoplay` started pauses while no part of it is in the window, and it plays again when a part comes back. The rule of the event `visible` (section 3) applies.
- One rule of sound applies on all targets, the rule of the strictest browser:
  - An element plays with sound only when its play starts in the batch of an event of the user. The events are a press, a key and a pointer button.
  - `autoplay` without `muted` is a compile error for a literal. With a computed `muted`, `autoplay` starts only while `muted` is true.
  - A play with sound at another time does not start, and the element stays paused.
  - A muted element that plays gets its sound only in the batch of an event of the user. At another time, it pauses.
- `surf lint` gives the finding `autoplay-stop` for a `Video` with `autoplay` and `loop`, without `controls` and without `bind:paused`.

**The media controls of the system** (ADR-0156).

- The media keys of the keyboard and the media controls of the system control one element. It is the element that last started to play with sound. The controls are "Now Playing" on macOS, the media controls of Windows, MPRIS on Linux, and the Media Session API on the web.
- "With sound" means that `muted` is false, `volume` is above 0 and the file has a sound track. A file shorter than 5 seconds does not connect, so a short sound of the UI does not take the media keys.
- The element stays connected while it plays and while it pauses, until another element connects or it leaves the tree.
- The system can play, pause and seek. A change through it is a change of the user: it writes the bound places and sends the events.
- "Next" and "previous" send `nexttrack` and `previoustrack`. The system shows these buttons only when the element has a handler for them.
- The system shows the title, the artist, the album and the picture of `metadata`, and the duration, the position and the rate. `artwork` is a path or a URL, as `src` of an `Image`. Without `metadata`, the title is `alt` and the picture is `poster`.
- On Linux, the name of the app on the session bus is `org.mpris.MediaPlayer2.` and the name of its binary. When another process has this name, the app adds `.instance` and its process id, as MPRIS suggests.

**Other rules.**

- An element that leaves the tree stops. `hidden` does not pause, as in HTML.
- The play goes on while the window is behind another window, or in the Dock or the taskbar.
- `alt` is the name for a screen reader. The role is "video" or "audio". `surf lint` gives the finding `no-name` for a `Video` without `alt` or `decorative`.

### 4.5c `Source` and `Track`

`Source` and `Track` are valid only in a `Video` or an `Audio`. A `Video` and an `Audio` take only `Source`, `Track` and blocks as children.

| Element | Attribute | Type | Default |
|---|---|---|---|
| `Source` | `src` | `String` | required |
| `Source` | `type` | `String`: a MIME type with codecs, as in HTML | none |
| `Track` | `src` | `String`: a WebVTT file | required |
| `Track` | `kind` | `String`: `captions` or `subtitles` | `subtitles` |
| `Track` | `srclang` | `String`: a language tag | none |
| `Track` | `label` | `String` | the value of `srclang` |
| `Track` | `default` | `Bool` | `false` |

**`Source`** (ADR-0156).

- Without `type`, the target must load the file to know its format. Give `type` with the codecs, so that a target skips a file that it cannot play.

**`Track`** (ADR-0157).

- `captions` give the speech and the sounds, for a person who cannot hear. `subtitles` translate the speech. The kinds `descriptions`, `chapters` and `metadata` are a compile error in version 1.
- The file is WebVTT. A literal path under `assets/` must name a file that parses as WebVTT. An error gives the line of the file.
- Version 1 has these parts of WebVTT:
  - The timings of each text cue.
  - The settings `line`, `position`, `size` and `align`.
  - The tags `<b>`, `<i>`, `<u>`, `<v>`, `<lang>` and `<c>`. A `<c>` gives no style.
  - The character references.
- The other parts of WebVTT are not in version 1. In a literal file, regions, vertical text, `STYLE` blocks, `<ruby>` and the timestamps in a text cue are a compile error. In a file from a URL, they show as plain text on the native targets.
- The text cues have the default look of WebVTT: white text on black with 80 % opacity, and a sans-serif font. The font size is 5 % of the height of the video. They are at the bottom of the video, and they move up while the bar of the controls shows.
- On the web, the browser draws the text cues. It applies the caption style of the system where it reads it, or its own caption settings. While the bar shows, the runtime sets the `line` of each text cue with an automatic line, so that the text cue ends above the bar.
- On the native targets, the runtime draws the text cues with the caption style of the system: MediaAccessibility on macOS, and `ClosedCaptionProperties` on Windows. Linux has no such setting, so the text cues have the default look of WebVTT. A change of the setting applies to the next frame.
- At the start, the first track with `default` shows. Without `default`, no track shows. The button of the captions chooses a track or "Off".
- A screen reader does not read the text cues aloud, as in each browser.
- `surf lint` gives the finding `no-captions` for a `Video` without `muted` and without a `Track` of the kind `captions`.
- Captions inside the media file, for example CEA-608 in an MP4, do not show in version 1.

### 4.5d The controls of `Video` and `Audio`

With `controls`, an element shows the bar of the runtime on all targets, also on the web (ADR-0156). The browser does not show its own controls. Not in version 0: in the full screen, the pointer does not hide. The Tab key also reaches the elements outside the video. The X11 fallback of Linux keeps its window. The button of the captions counts only the `Track` elements outside a block.

The bar is a component of the compiler. Only an app with `controls` has its code. On an `Audio`, the bar is the box of the element: a group with the name of `alt`. A `testid` of the element names the bar.

- The bar has these parts:
  - A button to play and to pause.
  - The time and the duration as `m:ss`, or as `h:mm:ss` from one hour.
  - A `Slider` for the position.
  - A button to mute, and a `Slider` for the volume.
  - A button for the captions, when the element has a `Track`. With more than one track, the button opens a `Menu` of the tracks and "Off".
  - On a `Video`, a button for the full screen.
- On a `Video`, the bar is at the bottom of the video, with white icons on a layer of 60 % black. On an `Audio`, the bar uses the theme tokens.
- The bar of a `Video` shows in three cases:
  - While the video does not play.
  - For 3 seconds after each move of the pointer over the video.
  - While a control of the bar has the focus.
- The bar takes 150 ms to fade in or out. With the setting "reduce motion", it shows and hides at once.
- A change through the bar is a change of the user. It writes the bound places and sends the events.
- `labels` gives the texts of the bar for a screen reader, with English defaults:

  ```surf
  type MediaLabels = {
      play: String = "Play",
      pause: String = "Pause",
      position: String = "Position",
      mute: String = "Mute",
      unmute: String = "Unmute",
      volume: String = "Volume",
      captions: String = "Captions",
      captionsOff: String = "Off",
      fullScreen: String = "Full screen",
      exitFullScreen: String = "Exit full screen",
  }
  ```

**The full screen.**

- A press on the button of the full screen shows the video, its text cues and its bar on the whole screen.
- On the web, the runtime calls `requestFullscreen()` on its element that holds the video, the text cues and the bar.
- On a native target, the window goes into the full screen mode of the system. The video then covers the window above each other element, on black, with `object-contain`.
- Escape, the button and the way of the system end the full screen. A window that was in the full screen mode before stays in it.
- In the full screen, only the video and its bar are in the accessibility tree. The Tab key reaches only the video and the controls of its bar, as in a modal `Overlay`. The focus stays on the button. The pointer hides with the bar.
- Only a press of the user starts the full screen. The program has no function for it in version 1.

**Keys.** The keys follow the WAI-ARIA Authoring Practices.

- Each button and each slider of the bar is a stop of the Tab key. They have the keys of a `Button` (section 4.6) and of a `Slider` (section 4.10).
- The keys move the slider of the position by 5 seconds, and its value stays exact, as the keyboard step of a WAI-ARIA slider. Its value for a screen reader is a text, for example "1:05 of 3:20". The slider of the volume has a step of 0.1.
- A `Video` with `controls` is also a stop of the Tab key. While it has the focus, Space plays or pauses, and the left and right arrow keys move the position by 5 seconds.
- The bar has no letter keys.

### 4.6 `Button`

| Attribute | Type | Default |
|---|---|---|
| `disabled` | `Bool` | `false` |

Events: `press`, `doubleclick`, `focus`, `blur`, `keydown`. `Button` accepts text.

### 4.7 `Input`

| Attribute | Type | Default |
|---|---|---|
| `value` | `String`, bindable | `""` |
| `placeholder` | `String` | `""` |
| `disabled` | `Bool` | `false` |
| `autofocus` | `Bool` | `false` |
| `selectOnFocus` | `Bool` | `false` |

Events: `input` (`text: String`), `change` (`text: String`), `submit`, `focus`, `blur`, `keydown`.

With `autofocus`, the control gets the keyboard focus at the end of the batch that puts it into the tree. When more than one control has it, the last one gets the focus. With `selectOnFocus`, the control selects its text each time it gets the focus. A press that gives the focus keeps this selection, unless a drag selects a part of the text, as in a browser.

Only a new `value` from the program replaces the text. A control without a binding keeps the text that the user typed, also after the loss of the focus (ADR-0113).

An `Input` has one line. It removes each line break from its text: from a `value` of the program and from a paste. The state keeps its line breaks until the next `input` event (ADR-0113).

Enter after an edit sends `change` and then `submit`. The loss of the focus after an edit sends `change` too, but only once for one edit (section 3.2, ADR-0113).

The text of an `Input` and of a `TextArea` stays selectable on all targets, also without `select-text` and with `select-none` (layout spec, section 5.10, ADR-0131). CSS UI 4 gives an editable element the used value `contain`, so `select-none` has no effect on a field, and `surf lint` gives the finding `field-select` for it. `selectOnFocus` selects the text, also with `select-none`.

### 4.8 `TextArea`

The attributes of `Input`. Events: `input` (`text: String`), `change` (`text: String`), `focus`, `blur`, `keydown`. `TextArea` has no `submit`, because Enter starts a new line.

### 4.9 `Toggle`

| Attribute | Type | Default |
|---|---|---|
| `checked` | `Bool`, bindable | `false` |
| `disabled` | `Bool` | `false` |

Events: `change` (`checked: Bool`), `focus`, `blur`, `keydown`. `Toggle` accepts text: the label.

### 4.10 `Slider`

| Attribute | Type | Default |
|---|---|---|
| `value` | `Float`, bindable | `50.0` |
| `min` | `Float` | `0.0` |
| `max` | `Float` | `100.0` |
| `step` | `Float` | `1.0` |
| `disabled` | `Bool` | `false` |

Events: `input` (`value: Float`), `change` (`value: Float`), `focus`, `blur`, `keydown`.

The slider rounds its value to a step from `min` and keeps it between `min` and `max`. The order of the attributes has no effect.

### 4.11 `Drawing` and the shapes

A `Drawing` is a box with its own coordinates, the view (ADR-0029). The shapes are valid only in a `Drawing`, or in a `Group` in a `Drawing`, and a `Drawing` takes only shapes and `Group`. Text is not a shape: put a `Text` above the drawing in a `Stack`.

```surf
<Drawing viewWidth={200.0} viewHeight={100.0} alt="Transfer curve" style="w-full">
    <Polyline points={curve} style="stroke-$text stroke-2 stroke-join-round" />
    <Circle
        cx={handle}
        cy={20.0}
        r={8.0}
        alt="Handle"
        style="fill-$accent"
        on:pointerdown={grab}
        on:pointermove={drag}
    />
</Drawing>
```

| Element | Attributes (`Float` unless the table says otherwise) |
|---|---|
| `Drawing` | `viewWidth`, `viewHeight`: the size of the view |
| `Line` | `x1`, `y1`, `x2`, `y2` |
| `Polyline`, `Polygon` | `points: List<Float>`: x and y, one after the other |
| `Rect` | `x`, `y`, `width`, `height`, `radius` (0 by default) |
| `Circle` | `cx`, `cy`, `r` |
| `Ellipse` | `cx`, `cy`, `rx`, `ry` |
| `Path` | `d: String`: SVG path data with the commands M, L, H, V, C, S, Q, T, A and Z, absolute and relative |
| `Group` | none: it groups shapes for `style`, `opacity` and `alt` |

- The view maps into the content box of the `Drawing` with the same scale on both axes, in the middle of the box. Without a width from its styles, a `Drawing` has the width of its view. Its height comes from the ratio of the view (layout spec, section 4).
- A shape has no layout. A change of an attribute of a shape paints again, but does not run the layout.
- The paint of a shape comes from the utilities `fill-`, `stroke-`, `stroke-<n>`, `stroke-dash-`, `stroke-cap-` and `stroke-join-` (layout spec, section 5.11), and from `opacity-`. A `Group` gives its paint to its shapes. The states, `dark:`, the directives and computed values work as on other elements: `hover:stroke-$accent`, `stroke-{color}`.
- A shape and a `Group` have no box. They take only these utilities, a text color (`text-$accent`, the color of a fill with no color of its own) and `cursor-`. Another utility is a compile error, for example `rotate-45`, `shadow-md` or `p-2` (ADR-0120). To move or turn a shape, change its coordinates, or put the transform on the `Drawing`.
- A shape gets the pointer events (section 3.1) when the pointer is inside its fill, or within half its stroke width of its stroke. A fill or a stroke of `none` gets no events.
- A `Drawing` with `alt` is an image for a screen reader. A shape with `alt` is a button when it has a `pointerdown` handler, else an image. A press of a screen reader on such a button sends `pointerdown` and `pointerup` in the middle of the shape.

### 4.12 `Icon`

```surf
import close from "./icons/close.svg";

prop heldIcon: Icon = close;

---
<Button on:press={cancel}>
    <Icon src={close} alt="Cancel" style="text-$accent" />
</Button>
<Icon src={held ? heldIcon : close} style="w-6 h-6" />
```

| Attribute | Type | Default |
|---|---|---|
| `src` | `Icon` | required |

No events of its own. It has the pointer events of section 3.1, and the whole box gets them: `x` and `y` are in the border box, as for an `Image`.

- An `Icon` is a value of the built-in type `Icon`. A prop, a state, a field or a list item can hold it. `==` compares two icons.
- The icon has the text color: a shape without a `fill`, and a `fill` or a `stroke` of `currentColor`, take the text color of the `Icon`. The other colors of the file stay.
- Without a size from its styles, an `Icon` has the height of its font size and the ratio of its view. `w-` and `h-` change each side: give both for another size. The view stays in the middle of the content box.
- `alt` makes it an image for a screen reader. Without `alt`, a screen reader skips it.
- An SVG file can have `svg` with `viewBox` (or `width` and `height`), `g`, `path`, `circle`, `ellipse`, `rect`, `line`, `polyline` and `polygon`, with the attributes `fill`, `stroke`, `stroke-width`, `stroke-linecap`, `stroke-linejoin`, `stroke-dasharray`, `opacity`, `fill-opacity` and `stroke-opacity`, also in a `style` attribute. Other elements, `transform` and `fill-rule="evenodd"` are compile errors. The `opacity` of a `g` multiplies the opacity of each of its shapes.
- `Icon.parse(text)` gives an icon from SVG text at run time, with the same rules (ADR-0030).

### 4.13 `Select` and `Option`

```surf
<Select bind:value={mode} on:change={v => save(v)} alt="Oversampling">
    <Option value="off">Off</Option>
    #for f in factors {
        <Option value={f}>{f}x</Option>
    }
</Select>
```

| Attribute | Type | Default |
|---|---|---|
| `value` (bindable) | `String` | the first option |
| `disabled` | `Bool` | `false` |
| `autofocus` | `Bool` | `false` |
| `system` | `Bool` | `false` |

Events: `change` (`value: String`), `focus`, `blur`, `keydown`.

An `Option` has `value: String` and `disabled: Bool`, and its text is its label. `Option` is valid only in a `Select`, and a `Select` takes only `Option` elements, an `OptionList` and blocks. Without a value that matches an option, a `Select` shows the first option.

On all targets, the closed box of a `Select` takes `bg-`, `border` and `rounded-`, and it has an arrow at its end. Without them, it has the colors `$surface` and `$border`.

A press opens the list of the runtime, which is the same on all targets (ADR-0059). The list is below the box, or above it when it does not fit. The arrow keys move the selection, Enter takes it, and Escape or a press outside closes the list. A press on a row takes it at once.

The list takes styles like each other element. An `Option` takes `style` for its row, with `hover:`, `focus-visible:` and `style:` directives. The optional `OptionList` holds the options, and its `style` applies to the box of the open list: its background, border, radius, padding, gap and size. Without styles, the list has the colors of the theme.

```surf
<Select bind:value={mode} style="w-30 rounded-8">
    <OptionList style="gap-0.5 p-1 rounded-8 border bg-$surface">
        #for f in factors {
            <Option value={f} style="px-2 py-1 rounded-4 hover:bg-$accent {f == mode}:font-600">
                {f}
            </Option>
        }
    </OptionList>
</Select>
```

With `system`, a `Select` opens the control of the system instead, and an app accepts its look and its timing. `system` takes no value from a state. On macOS this is the menu of the system, in dark with a dark app (ADR-0042). macOS reports a pick more than 400 ms after the click. On the web it is the list of the `<select>` of the browser (ADR-0040). On Linux, which has no such control, it is the list of the runtime. A `system` control takes no styles for its list.

### 4.14 `Overlay`

```surf
<Stack>
    <Button on:press={open = true}>Presets</Button>
    #if open {
        <Overlay side="bottom" on:close={open = false}>
            <Column style="bg-$surface p-2 rounded">...</Column>
        </Overlay>
    }
</Stack>
```

| Attribute | Type | Default |
|---|---|---|
| `side` | `String`: `top`, `bottom`, `left`, `right`, `start` or `end` | `bottom` |
| `offset` | `Float` | `4` |
| `modal` | `Bool` | `false` |
| `focus` | `Bool` | `false` |
| `passive` | `Bool` | `false` |

Events: `close`.

- An `Overlay` is above all other elements. No `overflow-hidden` or `Scroll` of its parents cuts it, and it takes no space in its parent. It is a flex column as large as its content.
- Its parent element is its anchor. It starts at the start edge of the anchor, `offset` from it on `side`. When it does not fit in the window there, it goes to the opposite side, and then it moves into the window.
- `start` and `end` are logical sides (ADR-0124). `start` is the left side and `end` is the right side, unless the overlay has the direction `rtl` (layout spec, section 5.2). Then they change places. So above and below an anchor, an overlay with the direction `rtl` starts at the right edge of the anchor.
- A press outside the last open `Overlay`, or Escape, sends it `close`. That press does not reach the element below. The app removes the overlay in its handler, or keeps it.
- With `modal`, the overlay covers the window with a dark layer, and its content is in the middle. A press on the dark layer sends `close`.
- With `modal` or `focus`, the first control in the overlay gets the focus when it opens. A control with `autofocus` wins (ADR-0102).
- Tab and Shift+Tab stay in a modal overlay. In an overlay with `focus`, Tab sends `close`.
- Each overlay gives the focus back when it closes, if the focus went away with it.
- With `passive`, a press outside it and Escape pass through it, and it sends no `close`. A tooltip uses this. On the web, the control with the focus has `aria-describedby` with the `id` of the overlay while it shows.
- With `modal`, it is a dialog for a screen reader, with `aria-modal`. Else `alt` makes it a named group. A `role` wins.

### 4.15 `Transition`

Props: `enter` (`String`), `exit` (`String`), `ms` (`Int`, default 150), `ease` (`String`, default `ease`), `delay` (`Int`, default 0), `stagger` (`Int`, default 0), `from` (`String`, default `start`), `move` (`Bool`).

```surf
<Transition enter="opacity-0 translate-y-2" exit="opacity-0" ms={200} ease="ease-out">
    #if open {
        <Column>...</Column>
    }
</Transition>
```

- `Transition` has no box. Its children are children of its parent for the layout and for the inherited styles. It takes no `style` and no events.
- `enter` gives the utilities of the start of an enter, and `exit` the utilities of the end of an exit. It needs at least one of them, or `move`. They can set `opacity`, `transform`, `background`, `color` and `shadow`. `exit` can also set `absolute` and the utilities of `inset`, also the logical sides `inset-s-` and `inset-e-` (ADR-0124). These apply at the start of the exit. A value replaces the whole property.
- `move` animates an element that changes its place, from its old place to its new place (ADR-0081). A `#for` needs a `key` for this. `move` needs no `enter` and no `exit`.
- `ms`, `delay` and `stagger` are numbers in the source, in ms. `ease` is the name of an easing utility of the layout spec, section 12.4, as `ease` of a `motion` (language spec 7.5, ADR-0104). Examples are `ease-linear`, `ease-out-cubic`, `ease-spring` and `ease-spring-0.4`. A reader can then copy the name from a `style`.
- A name without `ease-`, for example `spring` or `linear`, is a compile error. The message gives the correct name, for example `ease-spring`.
- `delay` holds back each enter, exit and move.
- `stagger` adds a time for each element of one batch, so that the rows of a list start one after the other (ADR-0104). The elements that start in one frame are one batch, in tree order. A new row that comes alone has no extra time. `from` gives the first element of the batch:

  ```surf
  <Transition enter="opacity-0 translate-y-2" ms={200} stagger={40} from="center">
      #for row in rows key row.id {
          <Text>{row.title}</Text>
      }
  </Transition>
  ```

  | `from` | The extra time of element `i` of `n` |
  |---|---|
  | `start` | `i * stagger` |
  | `end` | `(n - 1 - i) * stagger` |
  | `center` | `floor(abs(2 * i - (n - 1)) / 2) * stagger` |

- A row that starts by its index for a change of its style uses a computed delay: `delay-{i * 40}` (layout spec 12.1).
- An element animates if a `#if`, `#for`, `#match` or `#await` inside the `Transition` adds or removes it, and only blocks are between the two. A nested element goes with its parent. A text node goes at once. The rows of a `List` do not animate.
- The block goes inside the `Transition`. A `Transition` inside a block goes with that block.
- The children of its first update do not play an enter. A change of the theme or the window width plays no enter.
- With the setting of reduced motion on, an element comes and goes at once (layout spec 5.10).
- During an exit, the element keeps its place in the layout, gets no events and leaves the accessibility tree. It loses the focus. Its values no longer change. Its siblings close the gap at the end. With `exit="absolute"`, it leaves the flow at the start of the exit, and in a `Stack` this gives a cross-fade.
- A block with no other element can show again during an exit. The new element of the same kind then starts at the shown values of the old element, and the old element goes at once. A new row beside other rows does not do this: the row that leaves plays to its end. Two branches that change in the same frame both play.

### 4.16 Components that version 0 does not support yet

`Canvas` and `PlatformView` are in the language spec, section 9.6, but this reference does not define them yet. Each of them is a compile error in version 0.

- `Canvas` is a drawing function. Use a `Drawing` with shapes (section 4.11).
- `PlatformView` comes later.

---

## 5. The components of `surf:ui`

`surf:ui` is a built-in module (ADR-0102). Each component is a `.surf` file in the compiler, so it works on each target. An app that does not import a component has no code of it.

```surf
import { Dialog, Tabs, UiItem } from "surf:ui";
```

These rules apply to each component:

- It has the roles, the names and the keys of the WAI-ARIA Authoring Practices. On the native targets, the same roles go through AccessKit.
- It uses the theme tokens (layout spec, section 2.1), so it works in each variant of the theme.
- A `style` on an instance applies to its root elements (language spec, section 9.2).
- Its motion stops with the setting "reduce motion" of the system.
- A text in a component, such as `closeLabel`, is a prop with an English default.
- A component name of `surf:ui` is a name of the project. A file with the same name in the app is a compile error when the app imports that component.

`Tabs`, `RadioGroup`, `Accordion` and `Menu` take a list of `UiItem`:

```surf
type UiItem = { value: String, label: String, disabled: Bool = false }
```

The module also has `Tone`, the enum of `Badge` and `Toast`, with the cases `neutral`, `accent`, `success`, `warning` and `error`. A parent writes `tone={.warning}` with no import.

### 5.1 `Card`

A surface for one topic. It is a group, and `heading` is its name.

```surf
<Card heading="Plan">
    <Text>10 seats</Text>
    #fill footer {
        <Text>Renews in May</Text>
    }
</Card>
```

| Prop | Type | Default |
|---|---|---|
| `heading` | `String` | `""`: no heading |
| `level` | `Int` | `3`: the level of the heading, 2, 3 or 4. Another value is 3 |

Slots: `default`, `footer?`. The heading has the role `heading`. Give a card directly under the heading of the page the level 2, so that the levels of the page skip no level (ADR-0110).

### 5.2 `Badge`

A small label for a state or a count.

```surf
<Badge tone={.success}>Paid</Badge>
```

| Prop | Type | Default |
|---|---|---|
| `tone` | `Tone` | `.neutral` |

Slot: `default`, which accepts text.

### 5.3 `Separator`

A line between two parts of a page. It has the role `separator` and an orientation.

```surf
<Separator />
<Separator vertical />
```

| Prop | Type | Default |
|---|---|---|
| `vertical` | `Bool` | `false` |

### 5.4 `Avatar`

A picture of a person, or the initials of the name. It is an image, and `name` is its name.

```surf
<Avatar name="Ada Lovelace" src="assets/ada.png" />
<Avatar name="Grace Hopper" />
```

| Prop | Type | Default |
|---|---|---|
| `name` | `String` | required |
| `src` | `String?` | `none`: the initials |

### 5.5 `Spinner`

A wait with no known end. It has the role `progressbar` and no value. With "reduce motion", the ring stands still.

```surf
<Spinner alt="Loading the list" />
```

| Prop | Type | Default |
|---|---|---|
| `alt` | `String` | `"Loading"` |

### 5.6 `Progress`

A bar for the progress of a task. It has the role `progressbar`, with `valueNow`, `valueMin` and `valueMax`. Without `value`, the end is not known, and the bar pulses. With "reduce motion", the bar stands still.

```surf
<Progress alt="Upload" value={percent} />
<Progress alt="Waiting" />
```

| Prop | Type | Default |
|---|---|---|
| `alt` | `String` | required |
| `value` | `Float?` | `none` |
| `max` | `Float` | `100.0` |

### 5.7 `Radio`

One choice with a ring and a label. It is a `Button` with the role `radio`. Use a `RadioGroup` for a group: it gives the keys. A `Radio` alone suits one choice in a layout of the app.

```surf
<Radio checked={agree} on:press={agree = !agree}>I agree</Radio>
```

| Prop | Type | Default |
|---|---|---|
| `checked` | `Bool` | `false` |
| `disabled` | `Bool` | `false` |
| `tabStop` | `Bool` | `true`: the Tab key reaches it |
| `id` | `String` | `""` |

Events: `press`, `key` (`name: String`). Slot: `default`, which accepts text.

### 5.8 `RadioGroup`

A choice of one item. It has the role `radiogroup`.

```surf
<RadioGroup alt="Size" items={sizes} bind:value={size} />
```

| Prop | Type | Default |
|---|---|---|
| `items` | `List<UiItem>` | required |
| `value` | `String`, bindable | `""` |
| `alt` | `String` | required |
| `horizontal` | `Bool` | `false` |

Event: `change` (`value: String`).

| Key | Effect |
|---|---|
| Tab | Goes to the checked radio button, and then leaves the group. |
| Up, Left | Checks the previous item, around the ends, past a disabled item. |
| Down, Right | Checks the next item. |
| Home, End | Checks the first or the last item. |
| Space | Checks the item with the focus. |

### 5.9 `Tabs`

One panel at a time. The tabs have the role `tab`, and the panel has the role `tabpanel`. The panel follows the tab at once.

```surf
<Tabs alt="Settings" items={pages} bind:value={page}>
    #fill panel(item) {
        <Text>{item.label}</Text>
    }
</Tabs>
```

| Prop | Type | Default |
|---|---|---|
| `items` | `List<UiItem>` | required |
| `value` | `String`, bindable | `""`: the first item that is not disabled |
| `alt` | `String` | required |
| `rtl` | `Bool` | `false`. `true`: the tabs go from right to left, for right-to-left text (ADR-0122) |

Event: `change` (`value: String`). Slot: `panel(item: UiItem)`.

| Key | Effect |
|---|---|
| Tab | Goes to the chosen tab, and then to the panel. |
| Left, Right | Chooses the previous or the next tab, around the ends, past a disabled tab. With `rtl`, Left chooses the next tab, and Right the previous tab. |
| Home, End | Chooses the first or the last tab. |

The runtime does not know the direction of the text of an element, so an app in right-to-left text gives `rtl` to each `Tabs`.

### 5.10 `Accordion`

Sections that open and close. Each header is a heading of level 3 with a `Button`. Each open panel has the role `region`, and its header names it.

```surf
<Accordion items={sections} bind:open={shown} multiple>
    #fill panel(item) {
        <Text>{item.label}</Text>
    }
</Accordion>
```

| Prop | Type | Default |
|---|---|---|
| `items` | `List<UiItem>` | required |
| `open` | `List<String>`, bindable | `[]`: the values of the open sections |
| `multiple` | `Bool` | `false`: one section at a time |

Event: `change` (`value: String`). Slot: `panel(item: UiItem)`.

| Key | Effect |
|---|---|
| Enter, Space | Opens or closes the section. |
| Up, Down | Goes to the previous or the next header, around the ends. |
| Home, End | Goes to the first or the last header. |

### 5.11 `Dialog`

A modal dialog above a dark layer. It has the role `dialog` and `aria-modal`, and `heading` is its name.

```surf
<Dialog heading="Delete the file?" bind:open={asking}>
    <Text>This action cannot be undone.</Text>
    #fill actions {
        <Button on:press={asking = false}>Cancel</Button>
    }
</Dialog>
```

| Prop | Type | Default |
|---|---|---|
| `open` | `Bool`, bindable | `false` |
| `heading` | `String` | required |
| `closeLabel` | `String` | `"Close"`. Empty: no close button |

Event: `close`. Slots: `default`, `actions?`.

- When the dialog opens, the first control gets the focus. An `Input` with `autofocus` wins.
- Tab and Shift+Tab stay in the dialog, around its ends.
- Escape, a press on the dark layer and the close button close it, and send `close`.
- When the dialog closes, the control that opened it gets the focus back.

### 5.12 `Tooltip`

A short text for the control in it. The text shows while the control has the focus, and after a pointer rests on the control. It describes the control for a screen reader while it shows.

```surf
<Tooltip text="Saves the file">
    <Button on:press={save()}>Save</Button>
</Tooltip>
```

| Prop | Type | Default |
|---|---|---|
| `text` | `String` | required |
| `side` | `String`: `top`, `bottom`, `left`, `right`, `start` or `end`, as the `side` of an `Overlay` (section 4.14) | `"top"` |
| `ms` | `Int` | `500`: the rest of the pointer before the text shows |

Slot: `default`. Escape hides the text. The pointer can go onto the text, and the text stays (WCAG 1.4.13).

### 5.13 `Menu`

A button that opens a list of actions. The button has `hasPopup="menu"`, and the rows have the role `menuitem`.

```surf
<Menu label="Actions" items={actions} on:select={v => run(v)} />
```

| Prop | Type | Default |
|---|---|---|
| `label` | `String` | required |
| `items` | `List<UiItem>` | required |
| `disabled` | `Bool` | `false` |
| `alt` | `String` | `""`: the name of the button for a screen reader. Empty: the label. The name must hold the label, for voice control |
| `tabStop` | `Bool` | `true`. `false`: the Tab key does not reach the button, for a list with one stop |

Event: `select` (`value: String`).

| Key | Effect |
|---|---|
| Enter, Space, Down on the button | Opens the menu on the first item. |
| Up on the button | Opens the menu on the last item. |
| Up, Down | Goes to the previous or the next item, around the ends, past a disabled item. |
| Home, End | Goes to the first or the last item. |
| A letter | Goes to the next item that starts with the letter. |
| Enter, Space | Runs the item, and closes the menu. |
| Escape, Tab | Closes the menu. |

When the menu closes, the button gets the focus back.

### 5.14 `Toast`

A short message that goes away by itself. The region has the role `status` and is always in the tree, so a screen reader says each new message. With `tone={.error}`, it has the role `alert`.

```surf
<ToastList>
    <Toast bind:open={saved} tone={.success}>The file is saved.</Toast>
</ToastList>
```

| Prop | Type | Default |
|---|---|---|
| `open` | `Bool`, bindable | `false` |
| `tone` | `Tone` | `.neutral` |
| `ms` | `Int` | `5000`. 0: it stays until the user closes it |
| `closeLabel` | `String` | `"Close"` |
| `restart` | `Int` | `0`. A new value starts the time again, for a new message in an open toast |

Event: `close`. Slots: `default`, which accepts text, and `action?`, for a button such as "Undo".

```surf
<Toast bind:open={shown} restart={round} ms={10000}>
    {notice}
    #fill action { <Button on:press={undo()}>Undo</Button> }
</Toast>
```

A `Toast` is in the flow of its parent. A `ToastList` gives the toasts of a window their place.

### 5.15 `ToastList`

The place of the toasts of a window (ADR-0122). It is `fixed`, at the bottom right of the window (layout spec, section 5.7). The toasts in it stack upward in the order of the source, so two messages at the same time do not cover each other.

```surf
<ToastList>
    <Toast bind:open={saved} tone={.success}>The file is saved.</Toast>
    <Toast bind:open={failed} tone={.error} ms={0}>The file was not saved.</Toast>
</ToastList>
```

Slot: `default`. Put each `Toast` of a window in one `ToastList`.
