# Surface: the short form of the documents
This file has the full text of the first steps, and the headings, the first paragraphs and the Surf samples of the other documents.
# Getting started
- **Status:** current on 2026-11-06, for version 0.
- **Related documents:** [language spec](https://surface.solenovis.com/docs/language.md), [component reference](https://surface.solenovis.com/docs/components.md), [standard library](https://surface.solenovis.com/docs/standard-library.md), [limits of version 0](https://surface.solenovis.com/docs/limits.md)
This guide makes a small shopping list app. The app has three files: a module file with a record type, a component for one row, and the root component. At the end, the same app runs in a browser and in a desktop window.
---
## 1. Make the project
1. Install the `surf` command. It is the only tool that you install, and a web build needs nothing else:
```bash
curl -fsSL https://git.solenovis.com/henning.liemant/surface/raw/branch/main/scripts/install.sh | sh
```
The script puts `surf` into `~/.surf/bin`. Add this folder to the `PATH`. [The `surf` command](https://surface.solenovis.com/docs/commands.md) has the details.
2. Make a project with the name `groceries`:
```bash
surf new groceries
```
3. Start the development server:
```bash
surf dev groceries
```
4. Open `http://localhost:8000` in a browser. The page shows a counter. The command also prints the address of the [inspector](https://surface.solenovis.com/docs/inspector.md), the dev tools of the running app.
The server builds the app again after each change to a file in `groceries/src`, and the page loads again. Keep the server open for the next steps.
---
## 2. Read the first component
`surf new` wrote `groceries/src/App.surf`. Each project has this file. It is the root component.
```surf
state count = 0;
derived doubled = count * 2;
---
Count: {count}Doubled: {doubled}
```
A component file has two parts. A line `---` divides them.
- The part above the line has the declarations. `state count = 0` is a value that can change. `derived doubled` gets its value from other values. It changes when `count` changes.
- The part below the line has the markup: the elements of the UI. `{count}` shows a value in text. `on:press={count += 1}` runs a statement each time the button gets a press.
- `style` has utilities: `p-6` is a padding of 24 logical px, and `gap-3` is a space of 12 logical px between the children. A logical px is the same size on each screen. The web output writes it in `rem`, so the font size of the browser scales the app.
When `count` changes, the runtime updates only the two `Text` elements that use it. There is no virtual DOM.
---
## 3. Add a record type
Make the file `groceries/src/model.surfs`:
```surf
pub type Item = { id: Int, name: String, done: Bool = false }
pub fn left(items: List) -> String {
const n = items.filter(i => !i.done).count();
return n == 1 ? "1 item left" : "{n} items left";
}
```
A file without `---` and markup is a module file. It has types and functions.
- `pub` makes a declaration available to other files. Other declarations are private to the file.
- `type Item` is a record type. `done` has a default value, so a new item can leave it out.
- `items.filter(i => !i.done)` makes a new list with the items that are not done. The [standard library](https://surface.solenovis.com/docs/standard-library.md) lists all methods.
Values in Surf are values, not references. A function that gets `items` gets its own copy, and a change to the copy does not change the original.
---
## 4. Make a component for one row
Make the file `groceries/src/ItemRow.surf`:
```surf
import { Item } from "./model.surfs";
bindable prop item: Item;
event remove(id: Int);
---
{item.name}
```
- `import` names a file relative to this file: `./` is the same folder and `../` the folder above. The files can be in subfolders of `src`.
- `prop item: Item` is a value that the parent gives. `bindable` lets the component write the value back to the parent.
- `bind:checked={item.done}` connects the check box to the field `done`. A click on the check box writes the field, and the write goes to the parent through the bindable prop.
- `event remove(id: Int)` declares an event. `emit remove(item.id)` sends it to the parent.
---
## 5. Use the row in the root component
Replace the contents of `groceries/src/App.surf` with this:
```surf
import { Item, left } from "./model.surfs";
import ItemRow from "./ItemRow.surf";
state items: List = [];
state draft = "";
state nextId = 1;
fn add() {
const name = draft.trim();
if name.isEmpty() {
return;
}
items.push({ id: nextId, name: name });
nextId += 1;
draft = "";
}
fn remove(id: Int) {
items = items.filter(i => i.id != id);
}
---
Groceries
#for item in items key item.id {
} empty {
The list is empty.
}
{left(items)}
```
- `state items: List = []` needs the type, because an empty list does not show the type of its elements.
- `bind:value={draft}` connects the text field to `draft`. `on:submit={add}` calls `add` when the user presses Enter.
- `#for item in items key item.id { ... }` makes one `ItemRow` for each item. The key keeps the identity of a row when the list changes. The `empty` block shows when the list has no items.
- `bind:item={item}` gives each row its item, and the writes of the row change that item in `items`.
- `on:remove={remove}` calls `remove` with the `id` of the event.
- `$text-secondary` is a theme token. It has a value for each variant of the theme: light, dark, and two variants with high contrast. A token always has `$`. The [layout spec](https://surface.solenovis.com/docs/layout.md), section 2.1, lists the tokens.
Save the file. The page shows the list. Type a name, press Enter, and click the check box of an item. The text "1 item left" changes.
A long `style` text can go over more than one line. `surf fmt` breaks it at a space and puts each new line under the first utility (ADR-0160):
```surf
```
The line breaks change nothing: a utility ends at a space or at the end of a line.
---
## 6. Find the errors before you run the app
The compiler checks all files before it builds the app. Change `item.name` in `ItemRow.surf` to `item.title`, and run this command:
```bash
surf check groceries
```
The command shows the file, the line, the column and the message of each error. The development server shows the same errors in the page. Change the name back.
`surf lint` finds code that compiles but is probably wrong. Examples are a state that nothing reads, and a `#for` over records with no key:
```bash
surf lint groceries
```
The editor extensions in `editors/vscode` and `editors/zed` show the errors and the warnings while you type. They use `surf lsp`.
---
## 7. Test the app
`surf test` runs tests with no build, no browser and no window.
1. Give three elements of `groceries/src/App.surf` a `testid`:
```surf
{left(items)}
```
2. Make the file `groceries/tests/groceries.surfs`:
```surf
import { Test } from "surf:test";
import { Item, left } from "~/model.surfs";
pub fn testLeft() {
Test.equal(left([{ id: 1, name: "Milk" }]), "1 item left");
}
pub fn testAdd() {
Test.mount("App.surf");
Test.fill("draft", "Milk");
Test.press("add");
Test.equal(Test.text("left"), "1 item left");
}
```
3. Run the tests:
```bash
surf test groceries
```
The command shows `2 passed, 0 failed`.
- A test is a `pub fn` with a name that starts with `test`. It has no parameters, and it is not `async`.
- A test file is a module with a lowercase name. `~/model.surfs` is a path from `src`.
- `Test.mount("App.surf")` runs the app with no window. `Test.press` and `Test.fill` send real input events to the element with that `testid`.
- [Build a full app](https://surface.solenovis.com/docs/full-app.md), section 8, lists all functions of `Test`.
---
## 8. Build the app
1. Stop the development server.
2. Build the web app:
```bash
surf build groceries
```
3. Open `groceries/build/web/index.html` in a browser. The page needs no server. You can copy the folder `groceries/build/web` to any static web host.
4. Build the desktop app:
```bash
surf build groceries --target desktop
```
The command writes the binary into the folder of the system of this machine, for example `groceries/build/desktop/linux-x86_64/groceries` on Linux, and `groceries/build/desktop/macos-aarch64/groceries.app` on a Mac. Each machine can also build Windows: `--os windows --arch x86_64` writes `groceries/build/desktop/windows-x86_64/groceries.exe` ([the `surf` command](https://surface.solenovis.com/docs/commands.md), section 3). The first native build downloads a private Rust toolchain and the runtime pack into `~/.surf`, and it compiles the runtime, so it takes some minutes. Later builds take some seconds. A native build needs a linker: on macOS, run `xcode-select --install`, and on Linux, install the package `gcc`. `surf doctor` shows what the machine has.
5. Start the binary in `groceries/build/desktop/`. On macOS, on Windows, and on Linux with a Wayland compositor, the app opens in a window. The window has the same UI as the web page.
`surf dev groceries --target desktop` runs the app in a window and loads it again after each change, as the web server does.
A file `src/head.html` goes into the head of the page, before the stylesheet, in `surf build` and in `surf dev`. Use it for a favicon, or for a script that must run before the first paint. A `` in this file replaces the name of the project.
---
## 9. Use a package
A package is a folder of `.surf` and `.surfs` files from a git address or from a local path.
1. Add the package:
```bash
surf add charts https://example.com/charts.git --rev v1.2.0
```
2. Import it:
```surf
import { Bar } from "pkg:charts";
```
`surf add` writes `surf.toml` and `surf.lock`, and it copies the package to `src/pkg/charts`. Keep these two files in git. `surf install` makes the same copy on another machine. An install runs no code of a package. `pkg:charts` reads `lib.surfs` of the package, and `pkg:charts/x.surfs` reads another file.
---
## 10. Next steps
- The [component reference](https://surface.solenovis.com/docs/components.md) lists the built-in components, their attributes and their events.
- The [standard library](https://surface.solenovis.com/docs/standard-library.md) lists the methods of `String`, `List`, `Map` and the other built-in types.
- The [Surface Layout Spec](https://surface.solenovis.com/docs/layout.md), section 12, lists each utility of `style`.
- The [language spec](https://surface.solenovis.com/docs/language.md) has all rules: slots, effects, async functions, and extern modules for code in JavaScript or Rust.
- The [limits of version 0](https://surface.solenovis.com/docs/limits.md) lists the features that the compiler does not have yet.
- `examples/todomvc` is a larger app with a router and storage in extern modules.
- [Build a full app](https://surface.solenovis.com/docs/full-app.md) adds pages from files, a server, forms, sign-in, tests and a deployment. `examples/notes` has its code, and `examples/tickets` is a larger app with an API.
# Build a full app
Read getting started first. This guide adds what a real web app needs: pages from files, data from a server, forms, sign-in, an API, tests and a deployment. examples/notes holds all the code of this guide, and the CI builds it.
## 1. Decide where your data lives
Make this decision first, because version 0 limits it.
## 2. Make the routes
A project with a src/routes folder has no src/App.surf. Delete that file. Each folder is a part of a URL:
```surf
prop user: String;
slot default;
---
Notes
New note
@slot default
```
## 3. Load data on the server
A load.server.surfs file runs on the server only. layout gives the data of Layout.surf, and load gives the data of Page.surf:
```surf
import { Home, Session } from "~/model.surfs";
import { allNotes } from "~/store.server.surfs";
import { Wave } from "surf:wave";
pub async fn layout() -> Result {
return .ok({ user: Wave.cookie("user") });
}
pub async fn load() -> Result {
return .ok({ notes: allNotes() });
}
```
## 4. Store data in an extern module
An extern module has a declaration, and an implementation for each target. The name of store.server.surfs keeps it out of the browser:
```surf
pub extern module Disk {
fn read() -> String;
fn write(text: String);
}
```
## 5. Take a form
A form is a Column or another container with the role form. Its action names a function of actions.surfs after ?/. An Input needs a name. The form works with no script. With the script, the page stays and shows the result:
```surf
{problem(form)}
```
```surf
import { addNote } from "~/store.server.surfs";
import { Failure, Request, failure, redirect } from "surf:wave";
pub async fn save(request: Request) -> Result {
const title = (request.form["title"] ?? "").trim();
if title.isEmpty() {
return .err(failure(422, "A note needs a title"));
}
const note = addNote(title, request.cookies["user"] ?? "");
return .err(redirect("/notes/{note.id}"));
}
```
## 6. Sign in
Wave.setCookie(name, value, maxAge) works in a load of the server, an action, an endpoint, a hook and a remote function. Each cookie has HttpOnly, SameSite=Lax and Path=/. Each cookie also has Secure, except on localhost, 127.0.0.1 and [::1].
```surf
import { Failure, Request, Wave, redirect } from "surf:wave";
pub async fn signIn(request: Request) -> Result {
Wave.setCookie("user", request.form["name"] ?? "guest", 86400);
return .err(redirect("/"));
}
```
```surf
import { Request, Response, redirectResponse } from "surf:wave";
pub async fn before(request: Request) -> Response? {
if request.path == "/new" && !request.cookies.contains("user") {
return redirectResponse("/login");
}
return none;
}
```
## 7. Answer with JSON
A server.surfs file has a function for each method:
```surf
import { allNotes } from "~/store.server.surfs";
import { Request, Response, jsonResponse } from "surf:wave";
pub async fn get(request: Request) -> Response {
return jsonResponse(Json.encode(allNotes()));
}
```
## 8. Test the app
Use these four steps.
## 9. Deploy
surf build --target server writes build/server/-server: one binary and no other file. A server is a native build, so the first one downloads a private Rust toolchain and the runtime pack into ~/.surf (the surf command). Copy the binary to the host. It reads these environment variables:
## 10. What else an app needs
# Surface language specification, v0
This document specifies the .surf language: the file structure, Surf Script, the markup, the reactivity model, the styles and the extern modules. The Surface Layout Spec is a separate document. It has the utilities. Section 12 gives only a summary of them.
## 1. Conventions
The design rule behind every decision in this spec: one source, one meaning, on every target. If a feature cannot have the same meaning in JavaScript and in native code, the feature is not in the language.
## 2. Source files
### 2.1 Encoding
Source files are UTF-8. Line ends can be LF or CRLF. The formatter writes LF.
### 2.2 File kinds
The extension gives the kind of a file (ADR-0093). The name of a component is its file name without the extension. A component file must contain markup. A module file must not contain markup.
### 2.3 Sections of a component file
A component file has two sections, in this order:
```surf
prop label: String = "Clicks";
state count = 0;
---
```
### 2.4 Comments
The comments are the same in the declarations and in the markup:
## 3. Lexical structure
### 3.1 Identifiers
An identifier starts with a letter of any script or _. Its other characters are letters, digits, combining marks and _ (ADR-0076). $ marks a theme token, and a name cannot start with $ (ADR-0095).
### 3.2 Keywords
var is reserved. It is not a declaration: use let for a variable (ADR-0013).
```surf
pub type Entry = { type: String, for: String, self: Bool }
pub fn mine(e: Entry) -> Bool = e.self && e.type == "user";
```
### 3.3 Literals
### 3.4 Strings
### 3.5 Statement ends
A ; ends each statement and each declaration on one line (ADR-0094). A line end has no meaning in script, so a statement can use more than one line.
## 4. Types
### 4.1 Overview
There is no any type, no null other than none, and no implicit conversion between types.
### 4.2 Int
### 4.3 Float
### 4.4 String
### 4.5 List and Map
### 4.6 Records
```surf
type Todo = { id: Int, title: String, done: Bool = false }
```
### 4.7 Enums
```surf
enum Tone { info, warning, error }
enum Shape { circle(radius: Float), rect(width: Float, height: Float) }
```
### 4.8 Optional values
### 4.9 Result and errors
### 4.10 Type inference
## 5. Values
### 5.1 Value semantics
Every value in Surf Script is a value, not a reference (ADR-0009).
### 5.2 Places
A place is an expression that can receive a value:
### 5.3 Equality of primitive writes
A write of an Int, Float, Bool or String value that is equal to the current value does not count as a change (section 10.4). A write of a list, map, record or enum value always counts as a change.
### 5.4 Conversion to text
{expression} in a string or in markup text accepts each type with the trait Text (section 7.8, ADR-0134). String, Int, Float and Bool have it. For another type, {x} gives the result of x.text(). Other types are a compile error.
## 6. Statements and expressions
### 6.1 Statements
An assignment is a statement, not an expression. a = b = 0 is a compile error.
```surf
for todo in todos {
todos.push({ ...todo, id: nextId() }); // runs once for each element of the original list
}
```
### 6.2 Operators
From the highest to the lowest precedence:
### 6.3 Match
```surf
const area = match shape {
.circle(r) => 3.14159 * r * r,
.rect(w, h) => w * h,
};
```
### 6.4 Lambdas
A lambda captures const values and the reactive sources of its component (section 10.1). A lambda cannot capture a let variable. This rule prevents shared mutable state between a function and its creator. The lambda of filter, find, map, sortBy and sortWith is an exception: it runs before the call returns, so it can read a let variable.
### 6.5 Calls and named arguments
```surf
fn circle(x: Float, y: Float, r: Float, fill: Color = "#007aff", stroke: Color? = none) {
// ...
}
fn dot(x: Float, y: Float) {
circle(x, y, 3.0, fill: "#ff9500");
}
```
## 7. Declarations
### 7.1 Where each declaration is valid
A component script has no let. Mutable component data must be state, so every change is visible to the reactivity model.
### 7.2 Imports and visibility
```surf
import Counter from "./Counter.surf";
import { Todo, nextId } from "./model.surfs";
import { Haptics } from "pkg:haptics";
import close from "./icons/close.svg";
```
### 7.3 Constants and functions
```surf
const MAX_ITEMS = 100;
pub fn plural(n: Int, word: String) -> String {
return n == 1 ? "1 {word}" : "{n} {word}s";
}
fn double(x: Int) -> Int = x * 2;
```
Generic functions
```surf
pub fn first(items: List) -> T? {
return items.get(0);
}
```
```surf
pub type Box = { value: T }
pub enum Either { left(value: L), right(value: R) }
fn sample() {
const boxed: Box = { value: 7 };
const pair = (1, "one");
const (n, word) = pair;
}
```
### 7.4 Props
```surf
prop label: String = "Clicks";
prop items: List;
bindable prop value: String = "";
```
### 7.5 State, derived values and effects
```surf
state count = 0;
state todos: List = [];
derived remaining = todos.filter(t => !t.done).count();
effect {
const r = room;
Chat.join(r);
cleanup {
Chat.leave(r);
}
}
on change(filter) {
page = 0;
}
on mount {
focusFirstField();
}
on destroy {
saveDraft();
}
```
Values that move in time
```surf
state open = false;
motion x(ms: 400, ease: "ease-spring") = open ? 240.0 : 0.0;
---
```
### 7.5a Sequences
```surf
state ready = false;
sequence intro(play: ready) {
cue logo(ms: 400) = "fade";
cue title(gap: -200, stagger: 30) = "rise";
cue more(gap: -100) = "fade";
}
on mount {
ready = true;
}
---
One universal best option
```
A sequence orders the animations of several elements on one timeline (ADR-0152). It is the timeline form of version 1. Each cue is one animation of the sequence. An element plays a cue with the utility cue- (layout spec 5.10).
### 7.6 Events
```surf
event submit(text: String);
fn send() {
emit submit(draft);
}
```
The parent listens with on:submit={handler}. An event without a listener does nothing. Event handlers can be async functions. The runtime cancels a running handler task when its component is destroyed.
### 7.7 Slots
A slot is a part of a component that the parent fills with markup.
```surf
slot default;
slot header?;
slot row(bindable item: Todo);
```
```surf
@slot header
#for item in items key item.id {
@slot row(item)
}
@slot default
```
```surf
#fill header { Tasks }
#fill row(todo) { {todo.title} }
Markup outside a fill goes into the default slot.
```
### 7.8 Traits and impl blocks
A trait names methods that a type can have. An impl block gives the methods to a type (ADR-0134). Traits have the form of Rust.
```surf
// shapes.surfs
pub trait Shape {
fn area(self) -> Float;
fn describe(self) -> String = "a shape with the area {self.area()}";
}
pub type Circle = { r: Float }
pub type Square = { side: Float }
impl Shape for Circle {
fn area(self) -> Float = 3.14 * self.r * self.r;
}
impl Shape for Square {
fn area(self) -> Float = self.side * self.side;
}
impl Circle {
pub fn new(r: Float) -> Circle = { r: r };
pub fn grow(mut self, k: Float) {
self.r *= k;
}
}
```
```surf
// App.surf
import { Circle, Shape } from "./shapes.surfs";
derived area = Circle.new(2.0).area();
```
```surf
pub type Vector = { x: Float, y: Float }
impl Add for Vector {
type Output = Vector;
fn add(self, other: Vector) -> Vector = { x: self.x + other.x, y: self.y + other.y };
}
impl Mul for Vector {
type Output = Vector;
fn mul(self, k: Float) -> Vector = { x: self.x * k, y: self.y * k };
}
impl Text for Vector {
fn text(self) -> String = "({self.x}, {self.y})";
}
```
### 7.9 Type parameters with bounds, and trait objects
Bounds
```surf
pub fn largest(xs: List) -> T {
let best = xs[0];
for x in xs {
if x > best {
best = x;
}
}
return best;
}
pub fn label(value: impl Text) -> String = "Value: {value}";
pub type Page = { items: List, next: String? }
```
```surf
pub type Page = { items: List }
impl Text for Page {
fn text(self) -> String = "{self.items.count()} items";
}
impl Page {
pub fn first(self) -> T? = self.items.get(0);
pub fn map(self, f: (T) -> U) -> Page = { items: self.items.map(f) };
}
```
```surf
// Picker.surf
component Picker;
prop items: List;
bindable prop selected: T? = none;
---
#for item in items {
}
```
```surf
items={users} />
```
```surf
import { Shape, Circle, Square } from "./shapes.surfs";
state shapes: List = [Circle.new(1.0), Square { side: 2.0 }];
derived total = shapes.map(s => s.area()).sum();
```
## 8. Async code
```surf
state seconds = 0;
async fn tick() {
while true {
await interval(1000);
seconds += 1;
}
}
on mount {
tick();
}
```
The runtime cancels the task of tick when the component leaves the tree, so the loop needs no cleanup. ADR-0026 gives the reasons for this form in place of a Timer module with callbacks.
## 9. Markup
### 9.1 Elements
### 9.2 Attributes
### 9.3 Text
### 9.4 Interpolation
{expression} in text follows section 5.4. The value updates when its dependencies change.
### 9.5 Blocks and tags
The markup has two kinds of control words:
```surf
#if count > 10 {
...
} else if count > 5 {
...
} else {
...
}
```
```surf
#for todo, index in todos key todo.id {
...
} empty {
No tasks.
}
```
```surf
#match shape {
.circle(r) => Circle, radius {r}
.rect(w, h) => {
Rectangle{w} by {h}
}
}
```
```surf
#await profile {
Loading
} then user {
{user.name}
} catch error {
{error.message}
}
```
```surf
#for item, i in items key item.id {
@const label = "{i}: {item.name}"
}
```
```surf
#try {
} catch message {
The chart failed: {message}
}
```
### 9.6 Built-in components (version 0)
The component reference gives the attributes, the bindings and the events of each component. An attribute or an event that is not in the reference is a compile error.
### 9.7 The keyboard focus
## 10. Reactivity
### 10.1 Sources and consumers
### 10.2 Dependencies
The compiler finds the dependencies of each consumer at compile time. The dependencies of a consumer are:
### 10.3 Lambdas as props
A lambda that reads reactive sources is itself a consumer in the parent. When one of its dependencies changes, the parent sends the lambda again, and the child sees a changed prop. This keeps the dependency analysis static across component boundaries.
### 10.4 Writes
### 10.5 Batches
Writes do not change the UI at once. The runtime collects writes into a batch. A batch ends when the current event handler, effect, on mount block or task step (up to the next await) returns.
### 10.6 Guarantees
## 11. Component lifecycle
## 12. Styles (summary)
The styles of an element are utilities in its style attribute. The Surface Layout Spec v0, section 12, lists every utility. This section gives a summary.
```surf
Title
```
## 13. Extern modules
An extern module declares functions that each target implements in its own language.
```surf
// haptics.surfs
pub enum Strength { light, medium, heavy }
pub extern module Haptics {
fn impact(strength: Strength);
async fn isAvailable() -> Bool;
fallback {
fn impact(strength: Strength) {}
async fn isAvailable() -> Bool = false;
}
}
```
### 13.1 Types at the boundary
Parameters and return values must use only these types:
### 13.2 Implementation files
Implementation files are in the same folder as the module file. The name is ..:
### 13.3 Events
An extern module can send events to the app (ADR-0031). It declares each event, and a component listens with a declaration:
```surf
// plugin.surfs
pub extern module Plugin {
fn setParam(id: Int, value: Float);
event report(params: List, peak: Float);
fallback {
fn setParam(id: Int, value: Float) {}
}
}
```
```surf
import { Plugin } from "./plugin.surfs";
state level = 0.0;
on Plugin.report(params, peak) {
level = peak;
}
```
### 13.4 Calls
## 14. Platform information
Locale
```surf
state count = 3;
state price = 1234.5;
derived total = Locale.number(price, style: .currency(code: "EUR"));
derived files = match Locale.plural(count) {
.one => "{count} file",
_ => "{count} files",
};
---
{total}{files}
```
```surf
// labels.surfs
const PRICE = Locale.number(9.99); // compile error: a module constant reads Locale
pub fn price() -> String = Locale.number(9.99); // a function reads Locale at each call
```
## 15. Errors
### 15.1 Compile errors
Each compile error gives the file, the line, the column, a message and, where possible, a suggested fix.
### 15.2 Runtime errors
The text of a runtime error is runtime error: and a text of this table. The text is the same on each target.
## 16. Grammar summary
The grammar is a summary. It leaves out the statements, which end with ; (section 3.5). The sections above are the normative text where the two disagree. on error in lifecycle is valid only in the root component of an app.
## 17. Complete examples
### 17.1 Counter
```surf
prop label: String = "Clicks";
state count = 0;
derived doubled = count * 2;
---
{label}: {count}Doubled: {doubled}
#if count > 10 {
That is a lot of clicks
}
```
### 17.2 Todo list
Module files have no mutable data, so the next id is a state in the component.
```surf
// model.surfs
pub type Todo = { id: Int, title: String, done: Bool = false }
pub fn remainingText(todos: List) -> String {
const n = todos.filter(t => !t.done).count();
return n == 1 ? "1 task left" : "{n} tasks left";
}
```
```surf
import { Todo, remainingText } from "./model.surfs";
state todos: List = [];
state draft = "";
state nextId = 1;
fn add() {
if draft.trim().isEmpty() {
return;
}
todos.push({ id: nextId, title: draft.trim() });
nextId += 1;
draft = "";
}
---
todo.id}>
#fill row(todo) { {todo.title} }
{remainingText(todos)}
```
## 18. Decisions for version 1
On 2026-11-04, the project owner decided the open questions of version 1:
# Surface component reference, v0
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:
## 2. Props
A prop of a built-in component has one of these types:
## 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
{
if key == "Escape" {
editing = false;
}
}} />
```
### 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
}
```
### 3.2 Rules of all events
These rules apply to all events:
## 4. Components
### 4.1 Containers: Row, Column, Stack, Grid
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).
### 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.
### 4.4 Text
No attributes of its own. Events: doubleclick.
```surf
Run surf dev, then read the documents.
```
### 4.4a Span
A part of a Text with a style of its own (ADR-0111). The web element is .
### 4.5 Image
Events: load (no value) and error (error: MediaError) (ADR-0160). alt (section 1) is the text of the image for a screen reader.
```surf
```
### 4.5a Link
A link (ADR-0051): an on the web target, and a Button with the role of a link on the native targets.
### 4.5b Video and Audio
A video and a sound (ADR-0155, ADR-0156). The web elements are