# 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
- **Status:** current on 2026-11-06, for version 0.
- **Related documents:** [getting started](https://surface.solenovis.com/docs/getting-started.md), [SurfWave reference](https://surface.solenovis.com/docs/routes.md), [limits of version 0](https://surface.solenovis.com/docs/limits.md), [security](https://surface.solenovis.com/docs/security.md)
Read [getting started](https://surface.solenovis.com/docs/getting-started.md) 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.
- A project has no package manager. An extern Rust file can use the Rust standard library and the Surf runtime, but no other crate. So a Surf server has no client for a database.
- **For a real database, run an API beside the Surf server.** Set `API_ORIGIN` to its address. A `load` function calls it with `Http.get("/api/x")`, and the server forwards the cookies and the `authorization` header. Only a relative URL gets these headers: an absolute URL goes out without them. A request from the browser to `/api/` goes to the same address, so the API must check each request. `examples/tickets` works this way.
- **For a small app, a file is enough.** This guide uses a file, so that the guide has one process.
---
## 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:
```
src/
model.surfs the types
store.server.surfs the notes (server only)
store.server.native.rs the file of the notes
hooks.server.surfs the check for each request
routes/
Layout.surf the frame of each page
Page.surf /
Error.surf the page for a failed load
load.server.surfs the data of Layout.surf and Page.surf
notes/[id]/Page.surf /notes/1
notes/[id]/load.server.surfs
new/Page.surf /new
new/actions.surfs the form of /new
login/Page.surf /login
login/actions.surfs the form of /login
api/notes/server.surfs GET /api/notes
```
`surf routes examples/notes` lists the routes. `surf dev` serves them. A project with server code also needs its server, so `surf dev` builds the server binary in the background and sends it the data requests, the forms and the endpoints. The first build is a native build, and it takes some minutes. A page shows its data when the output of `surf dev` says `the server code runs`.
A layout shows its page with `@slot default`:
```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() });
}
```
- `~/` is a path from `src`.
- The result types must be in a file that the browser can read, such as `model.surfs`. A type in a server file is a compile error, because the browser reads the data too.
- A page declares a field of the data as a prop: `prop notes: List`.
- `.err(notFound())` shows `Error.surf` with status 404. `.err(failure(403, "No"))` gives another status, and `.err(redirect("/login"))` goes to another page.
- The browser reads the value of each `load` function, also on the server. Return only what the page shows.
- `Wave.env` in a file that the browser also reads takes only a name in quotes that starts with `PUBLIC_`. Read a secret in a `*.server.surfs` file.
- Check a route parameter before you put it into a URL. `/` and `?` in a value change the address. Use a matcher: `[id=int]` or `[id=uuid]` as a folder name.
---
## 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);
}
```
The implementation is `store.server.native.rs`:
```rust
pub fn read() -> String {
std::fs::read_to_string("notes.json").unwrap_or_default()
}
pub fn write(text: &String) {
let _ = std::fs::write("notes.json", text);
}
```
A `String` parameter is `&String` in Rust. `surf extern stubs ` writes a stub with the correct types. `Json.encode` and `Json.decode` turn records into text. Write a file with one writer: the server handles requests on more than one thread.
A relative path such as `notes.json` is in the working folder of the server. The server must be able to write into that folder. The unit of `--adapter systemd` makes `/var/lib/` the working folder, and the other folders of the system are read-only for the app.
---
## 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)}
```
The action reads `request.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}"));
}
```
The result reaches the page in the prop `form`, as JSON text. `Json.decode` gives the `Failure` back. A `redirect` is the usual end of an action that worked.
A form from another site gets status 403. The server checks the `origin` of each form. An action must still check that the user can do the action.
---
## 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("/"));
}
```
`examples/notes` trusts the name, so that the guide stays short. A real app checks a password in its API, and it stores a random session id in the cookie, not a name. The hook must then ask the API about that id.
A hook protects pages. `before` runs for each request, so the check also holds for a request with no script and for the data of the page:
```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;
}
```
- `request.path` has one form. The server sends `//new`, `/%6eew` and `/new/` to `/new` before the hook runs, so `==` is safe.
- For a folder, compare the folder and the text under it: `request.path == "/admin" || request.path.startsWith("/admin/")`. Do not use `startsWith("/admin")` alone, because `/administration` matches it too. The server refuses `%2F` and `%5C` in a path, so `/admin%2Fusers` cannot pass this guard and reach a route with `[...rest]`.
- A remote function has the path `/__remote//`, and an endpoint has its own path. Guard these paths too.
- A `redirect` to a value of a user can leave your site. Accept only a path: [security](https://surface.solenovis.com/docs/security.md), section 4, has a function for this.
A `layout` function can also guard a folder. `.err(redirect("/login"))` in that function stops each page in the folder, and the data of each page. [Security](https://surface.solenovis.com/docs/security.md) lists what the server does, and what an app must do itself.
---
## 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()));
}
```
For code that only your own pages call, a remote function needs less: see the [SurfWave reference](https://surface.solenovis.com/docs/routes.md), section 6.
---
## 8. Test the app
Use these four steps.
1. `surf check ` checks the types of the whole project. Run it first.
2. `surf lint ` finds code that compiles but is probably wrong. A finding gives a non-zero exit code, so a CI job can fail on it. A comment `lint-ignore ` on the line of a finding silences it. The rules are `name-case`, `for-key`, `unused`, `no-effect`, `no-name` (ADR-0070), `contrast` (ADR-0103), `duplicate-id` (ADR-0106), and `media-format`, `no-captions` and `autoplay-stop` (ADR-0155 to ADR-0158).
3. `surf test ` runs each `pub fn test...` of the `.surf` files in `tests/` (ADR-0069). `--filter ` runs only the tests with that word in the name. This is `examples/notes/tests/pages.surfs`:
```surf
import { Test } from "surf:test";
pub fn testTheFormOfANewNoteStartsWithNoProblem() {
Test.mount("routes/new/Page.surf");
Test.equal(Test.text("form"), "");
Test.expect(Test.buttons("Save") == 1, "the form has one button Save");
}
```
A test runs no server. It reads the files that the browser reads, so it cannot import a server file. `Test.mount` mounts one component, for example one page of a route. A prop with no default value gets its value from `props`, a JSON object: `Test.mount("Row.surf", props: "\{\"label\": \"Milk\"\}")`. The JSON of a value is the JSON of `Json.encode`, so `Json.encode` of a record of the props also gives it. Test the server code with `curl`, as the end of this section shows.
The functions of `surf:test`:
| Function | What it does |
|---|---|
| `Test.expect(condition, message)` | Fails the test if the condition is false. |
| `Test.equal(found, expected)` | Fails the test if the two strings differ. |
| `Test.fail(message)` | Fails the test. |
| `Test.mount(file, props: json)` | Runs the component of the file with no window. `props` is optional. |
| `Test.press(id)` | Sends a press to the center of the element with that `testid`. |
| `Test.pressText(text)` | Sends a press to the button with that text or that `alt`. One button must have it. |
| `Test.hover(id)` | Moves the pointer to the center of the element with that `testid`. The pointer stays there. |
| `Test.fill(id, text)` | Writes the text into the field with that `testid`. |
| `Test.key(key)` | Sends a key to the element with the focus, for example `"Tab"`, `"Enter"` or `"a"`. |
| `Test.text(id)`, `Test.value(id)` | Reads the text of an element, or the value of a field. |
| `Test.count(id)` | Counts the elements with that `testid`. |
| `Test.buttons(text)` | Counts the buttons with that text or that `alt`. |
| `Test.focused()` | Reads the `alt` or the text of the element with the keyboard focus. With no focus, it gives `""`. |
| `Test.color(id)` | Reads the text color of the element with that `testid`, as `#rrggbb`. |
| `Test.role(text)`, `Test.description(text)` | Reads the role, or the description, that a screen reader gets for the button with that text. |
| `Test.access()` | Reads the accessibility tree: one line for each node, with its role and its name. |
| `Test.wait(ms)` | Moves the clock of the test by `ms` milliseconds. No real time passes. Timers, tasks and animations see the new time. |
| `Test.settle()` | Waits in real time for tasks that wait for another thread, for example a request. It stops when the text does not change for 25 frames, or after 2 seconds. |
| `Test.resize(width, height)` | Gives the window another size in logical px, for example for a narrow layout. The default is 800 × 600. |
Use `Test.wait` for a time in the app, for example a toast that closes after 10 seconds. Use `Test.settle` for a request: `Test.wait` gives the request no real time.
A test runs the native layout, not the CSS of the web. An extern module needs a host function or a `fallback` block in a test.
4. Test the build in a browser with Playwright. On the web, a `testid` is the attribute `data-testid`:
```javascript
test("a new note shows", async ({ page }) => {
await page.context().addCookies([{ name: "user", value: "ada", url: "http://localhost:3000" }]);
await page.goto("http://localhost:3000/new");
await page.locator("[data-testid=title]").fill("Milk");
await page.locator("[data-testid=save]").click();
await expect(page.locator("[data-testid=title]")).toHaveText("Milk");
});
```
Test a server with `curl`: `tests/server.sh` of this repository is a complete example.
---
## 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](https://surface.solenovis.com/docs/commands.md)). Copy the binary to the host. It reads these environment variables:
| Variable | Meaning |
|---|---|
| `HOST` | The address. The default is `127.0.0.1`, so a proxy on the same machine can reach the server and other machines cannot. A container needs `0.0.0.0`, and the `Dockerfile` of `--adapter docker` sets it. |
| `PORT` | The port. |
| `ORIGIN` | The address of the site in the browser, for example `https://example.com`. |
| `API_ORIGIN` | The address of your API. |
| Your own names | `Wave.env("NAME")` reads them in a file of the server. |
The binary has no TLS, so a public site needs a proxy in front of it. Set `ORIGIN` for that site: a proxy that changes `Host` makes the server refuse each form. [Security](https://surface.solenovis.com/docs/security.md) has the steps, and the settings of each limit.
A binary runs on the system that built it, so build on Linux for a Linux host. The `Dockerfile` of `--adapter docker` copies that binary into an image:
```bash
surf build examples/notes --adapter docker
```
```bash
surf build examples/notes --adapter systemd
```
An app with no server code can use `--target static`, or the adapters `netlify`, `vercel`, `cloudflare` and `github-pages`. The [SurfWave reference](https://surface.solenovis.com/docs/routes.md), section 5, has the list. A serverless host cannot run a Surf server, because the server is a binary with a port.
---
## 10. What else an app needs
| Need | Where |
|---|---|
| Colors, fonts and dark mode | `src/style.toml`: [layout spec](https://surface.solenovis.com/docs/layout.md), section 2.1 |
| Images and other files | `src/assets/`: [component reference](https://surface.solenovis.com/docs/components.md), section 4.5 |
| Icons | [Component reference](https://surface.solenovis.com/docs/components.md), section 4.12 |
| Long lists | `List`: [component reference](https://surface.solenovis.com/docs/components.md), section 4.3 |
| Dialogs and menus | `Overlay` and `Select`: [component reference](https://surface.solenovis.com/docs/components.md) |
| Screen readers | `alt`, `role` and the other attributes: [component reference](https://surface.solenovis.com/docs/components.md), section 1 |
| Animation | `Transition` and `transition-` utilities: [layout spec](https://surface.solenovis.com/docs/layout.md), section 12 |
| Animations that follow the scroll, and the split of a text | [Layout spec](https://surface.solenovis.com/docs/layout.md), section 5.10 |
| Animations in an order | `sequence`: [language spec](https://surface.solenovis.com/docs/language.md), section 7.5a |
| Page transitions | `Wave.animateRoutes(true)`: [SurfWave reference](https://surface.solenovis.com/docs/routes.md), section 7 |
| Gradients, shadows, blurs and blend modes | [Layout spec](https://surface.solenovis.com/docs/layout.md), section 5.8 |
| Video and sound | `Video` and `Audio`: [component reference](https://surface.solenovis.com/docs/components.md), sections 4.5b to 4.5d |
| Numbers, dates and lists in the language of the user | `Locale`: [standard library](https://surface.solenovis.com/docs/standard-library.md), section 6a |
| Methods that several types share | Traits, bounds and `dyn`: [language spec](https://surface.solenovis.com/docs/language.md), sections 7.8 and 7.9 |
| Code in Rust or JavaScript | Extern modules: [language spec](https://surface.solenovis.com/docs/language.md), section 13 |
| A desktop app | `surf build --target desktop`. An extern event reaches `surf dev` too. `[desktop] systems` of `surf.toml` lists the systems, also Windows, and `surf package` makes the installers: [the `surf` command](https://surface.solenovis.com/docs/commands.md), section 3 |
| What the server does for safety, and what your app must do | [Security](https://surface.solenovis.com/docs/security.md) |
| What does not exist yet | [Limits of version 0](https://surface.solenovis.com/docs/limits.md) |
# Surface language specification, v0
- **Status:** draft
- **Edition:** none yet. Version 0 has no stability guarantee.
- **Related documents:** [PLAN.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/PLAN.md), [ADRs](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/adr), [GLOSSARY.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/GLOSSARY.md), [component reference](https://surface.solenovis.com/docs/components.md)
- **Changes:** 2026-11-06: the texts of the runtime errors are sentences in Simplified Technical English (15.2, P6.29). 2026-11-06: the formatter writes 4 spaces for each indent level, and the key `indent` of `[fmt]` in `surf.toml` can choose another indent (2.1, ADR-0162). 2026-11-06: the compiler has sequences on all targets: `sequence`, `cue` and the utility `cue-` (3.2, 7.5a, 16, ADR-0152). `with` takes the name of an earlier cue, as in the grammar. 2026-11-06: the compiler has `Video` and `Track` (9.6, P6.25). 2026-11-06: the states `active:` and `focus-visible:` of ADR-0161 in 12. 2026-11-06: the compiler has `Locale` with the ICU of the system, and `sortWith` (4.4, 4.5, 7.3, 10.2, 10.4, 14, P6.6). It reads the key `locales` of `surf.toml`, and it gives its errors. The three sorts have one merge sort on each target, and section 4.5 gives its exact form. 2026-11-05: the compiler has `dyn` types and the error of `==` on a `dyn` value (5.1, 7.9, P6.3). It has generic components (3.2, 7.9, 9.1, P6.4), and `Comparable` includes `Equatable` (7.8). 2026-11-05: `Video`, `Audio`, `Source` and `Track` in the list of built-in components (9.6, ADR-0156, ADR-0157). 2026-11-05: sequences, the timeline form of version 1, with the contextual keywords `sequence` and `cue` (3.2, 7.1, 7.5a, 16, ADR-0152). The spec marks them "Not in version 0". 2026-11-05: the compiler has bounds with `+` and `impl` types (7.3, 7.9, P6.2). It also has impls and methods with type parameters, and `T.zero()` and `T.Item` in a generic function (7.8, 7.9). `==` on a type parameter needs `T: Equatable` (7.3). A generic function is a value where the expected type gives its types, as in Rust (7.3). 2026-11-05: the compiler has the rules of file names (2.2), the order of UTF-16 code units on the web (4.4), only the names of utilities in `ease` (7.5), and the import of a trait (7.2, 7.8). 2026-11-05: the answers of the project owner for version 1 (18). File names in camelCase and PascalCase (2.2, ADR-0140). Each keyword is valid as a field name (3.2, ADR-0134). The web compares strings by UTF-16 code units (4.4, ADR-0113). `ease` of `motion` takes only the name of a utility (7.5, ADR-0104). A file calls a method of a trait only when it imports the trait (7.2, 7.8, ADR-0134). The spec marks the new rules "Not in version 0". 2026-11-05: the compiler has traits and impl blocks (7.8, P6.1). The six words of traits are keywords (3.2). `sort`, map keys, `==`, `{x}` and the operators use the impls (4.5, 5.1, 5.4, 6.2).
2026-11-04: the details of `Locale` (ADR-0132). A module constant cannot read `Locale` (7.3, 14). A call of a function prop reads `Locale` when a function value of its type reads it (10.2). `Locale.set` is a write (10.4, 14). `locales = []` is an error, the lookup of RFC 4647 finds the locale, and a request after `Locale.set` has the header `x-wave-locale` (14). The three sorts use one stable merge sort on each target (4.5). 2026-11-04: `Locale` is valid in each function (7.3, 10.2, 14, ADR-0132). A project with `Locale` needs the key `locales`. A request of a server has its own locale, and `Locale.set` is not saved (14, ADR-0132). `sortWith` (4.4, 4.5, 6.4, ADR-0132). `==` on a `dyn` value is a compile error, and `==` on a type parameter needs `Equatable` (5.1, 7.3, 7.8, 7.9, ADR-0135). A module constant gets its value at its first read (7.3, ADR-0131). 2026-11-04: the decisions for version 1 (18). Traits and impl blocks (7.8, ADR-0134). Type parameters with bounds, generic components and trait objects (7.9, ADR-0135). The changes for both are also in 3.2, 4.1, 4.5, 5.1, 5.4, 6.2, 7.1, 7.3, 9.1, 10.2 and 16. No function types, traits or type parameters in extern modules (13.1, ADR-0133). The module `Locale` (7.2, 14, ADR-0132). The second section 13.3 (Calls) is now 13.4. 2026-11-03: a runtime error in a mount and in `on error` shows in a development build (15.2, ADR-0122). 2026-11-03: a module constant has one value (7.3, ADR-0118). 2026-11-03: a name cannot contain `__` (3.1, ADR-0115). 2026-11-03: `Span`, a `Link` in a `Text`, and one text block for each `Text` (9.3, 9.6, ADR-0111). 2026-11-03: `Window` (14, ADR-0106), the module `surf:ui` (7.2, ADR-0102), `Focus.to` and `Focus.newId` (9.7, ADR-0102), the high contrast modes of `ThemeMode` (ADR-0103), and `motion` (7.5, ADR-0104). 2026-11-02: the syntax review: `;` and match commas (3.5, 6.3, ADR-0094), `.surfs` for modules (2.2, ADR-0093), `$` only for tokens (3.1, ADR-0095), conditions in style text (9.2, 12, ADR-0096), number marks and enum attributes (3.3, 4.7, ADR-0099). 2026-11-01: generic records and enums, and tuples (7.3, ADR-0087). 2026-11-01: `Theme.setScale` (ADR-0085). 2026-11-01: `Set` and `{...value}` (ADR-0083), and `Int64` (ADR-0084). 2026-11-01: generic functions (7.3, ADR-0080). 2026-11-01: the functions of dates (ADR-0079). 2026-11-01: `File.pick` (ADR-0078). 2026-11-01: `Http.event` (ADR-0077). 2026-11-01: names in other scripts (3.1, ADR-0076). 2026-11-01: `Storage` (ADR-0074), and `on key` (7.5, ADR-0075). 2026-11-01: `#try` (9.5, 15.2, ADR-0073). 2026-11-01: packages and `pkg:` imports (7.2, ADR-0071), `surf lint` (3.1, 9.5, ADR-0070), and the module `surf:test` (7.2, ADR-0069). 2026-10-31: `Json.embed`, `Theme.designWidth`, and events and calls for each app in a process (13, ADR-0056). 2026-10-31: the variant form `style:{value}={{ ... }}`, and style text can have line ends (2, 9.2, ADR-0054). 2026-10-30: a function value can be each kind of value, and it only reads (6.4, 10.3, ADR-0039). 2026-10-29: `style:` directives and computed values in `style` on an instance of a user component (9.2, Phase 3, P3.11).
2026-10-29: `on error(message)` in the root component (15.2, Phase 3, P3.11).
2026-10-29: imports from subfolders, and a unique name for each component (7.2, Phase 3, P3.11).
2026-10-29: the pointer events `pointerdown`, `pointermove`, `pointerup` and `wheel` with the built-in record `Pointer` (component reference 3.1, Phase 3, P3.2).
2026-10-29: the standard library for apps (Phase 3, P3.1): math, `format`, `replace`, `indexOf`, `join`, `any`, `all`, `sum`, `Float.pi`, `Time.now()` and `Random`. The [standard library](https://surface.solenovis.com/docs/standard-library.md) lists the rules.
2026-10-29: `surf extern stubs` writes the stubs of the implementation files (13.2).
2026-10-29: an initial value cannot call a function of the component (7.5). Before, the compiler accepted the call, and the generated code failed.
2026-10-29: `List` is virtualized, has the list role, and takes `rowHeight` (9.6, ADR-0027).
2026-10-29: the `Timer` module is removed. The built-in `interval(ms)` ends at the next multiple of `ms` on the clock, so a loop with it does not drift (7.5, 8, ADR-0026).
2026-10-29: `Int.parse` and `Float.parse` read a number from a string (4.2).
2026-10-29: the bounds of `slice`, `get` outside a list, and `split("")` are the same on all targets (4.4, 4.5). Before, the web and the native targets gave different results.
2026-10-29: the features that the compiler of version 0 does not have yet are marked "Not in version 0" (3.1, 6.4, 7.2, 7.5, 9.2, 9.5, 9.6, 13.2, 15.2). [Limits of version 0](https://surface.solenovis.com/docs/limits.md) lists them on one page. `panic` ends a path like `return` (7.3). The lambda of a list method can read a `let` variable (6.4). Text can go into each slot that is inside a component that accepts text, not only the `default` slot (9.3).
2026-10-29: the rules of `chars`, `remove`, `sort` and `sortBy`. `enumerate` is removed: version 0 has no tuples, and a `for` loop gives the index (4.4, 4.5).
2026-10-28: a component file has no `