# 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. <!-- https://surface.solenovis.com/docs/full-app.md --> # 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; --- <Column style="p-6 gap-4 max-w-160"> <Row style="gap-3 items-center"> <Link href="/">Notes</Link> <Link href="/new">New note</Link> </Row> @slot default </Column> ``` ## 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<Session, String> { return .ok({ user: Wave.cookie("user") }); } pub async fn load() -> Result<Home, String> { 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 <Column role="form" method="post" action="/new?/save" style="gap-2"> <Input name="title" alt="Title" value="" /> <Button>Save</Button> <Text>{problem(form)}</Text> </Column> ``` ```surf import { addNote } from "~/store.server.surfs"; import { Failure, Request, failure, redirect } from "surf:wave"; pub async fn save(request: Request) -> Result<String, Failure> { 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<String, Failure> { 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/<name>-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 <!-- https://surface.solenovis.com/docs/language.md --> # 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; --- <Button on:press={count += 1}>{label}: {count}</Button> ``` ### 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<T>(items: List<T>) -> T? { return items.get(0); } ``` ```surf pub type Box<T> = { value: T } pub enum Either<L, R> { left(value: L), right(value: R) } fn sample() { const boxed: Box<Int> = { value: 7 }; const pair = (1, "one"); const (n, word) = pair; } ``` ### 7.4 Props ```surf prop label: String = "Clicks"; prop items: List<Todo>; bindable prop value: String = ""; ``` ### 7.5 State, derived values and effects ```surf state count = 0; state todos: List<Todo> = []; 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; --- <Column style="translate-x-{x}px" /> ``` ### 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; } --- <Column style="gap-4 items-center"> <Image src="assets/logo.svg" alt="Surface" style="cue-logo" /> <Text style="text-48 split-letters cue-title">One universal best option</Text> <Button style="cue-more">Read more</Button> </Column> ``` 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-<name> (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 <Column> @slot header #for item in items key item.id { @slot row(item) } @slot default </Column> ``` ```surf <TodoTable bind:items={todos}> #fill header { <Text style="text-20 font-600">Tasks</Text> } #fill row(todo) { <Toggle bind:checked={todo.done}>{todo.title}</Toggle> } <Text>Markup outside a fill goes into the default slot.</Text> </TodoTable> ``` ### 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<Float> 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<T: Comparable>(xs: List<T>) -> 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<T> = { items: List<T>, next: String? } ``` ```surf pub type Page<T> = { items: List<T> } impl<T: Text> Text for Page<T> { fn text(self) -> String = "{self.items.count()} items"; } impl<T> Page<T> { pub fn first(self) -> T? = self.items.get(0); pub fn map<U>(self, f: (T) -> U) -> Page<U> = { items: self.items.map(f) }; } ``` ```surf // Picker.surf component Picker<T: Text>; prop items: List<T>; bindable prop selected: T? = none; --- <Column style="gap-1"> #for item in items { <Button on:press={selected = item}>{item}</Button> } </Column> ``` ```surf <Picker items={users} bind:selected={chosen} /> <Picker<User> items={users} /> ``` ```surf import { Shape, Circle, Square } from "./shapes.surfs"; state shapes: List<dyn Shape> = [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 { <Text>No tasks.</Text> } ``` ```surf #match shape { .circle(r) => <Text>Circle, radius {r}</Text> .rect(w, h) => { <Text>Rectangle</Text> <Text>{w} by {h}</Text> } } ``` ```surf #await profile { <Text>Loading</Text> } then user { <Text>{user.name}</Text> } catch error { <Text style="text-$error">{error.message}</Text> } ``` ```surf #for item, i in items key item.id { @const label = "{i}: {item.name}" <Button alt={label}>{label}</Button> } ``` ```surf #try { <Chart data={points} /> } catch message { <Text>The chart failed: {message}</Text> } ``` ### 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 <Column style="p-6 gap-3 bg-$surface rounded max-600:p-4"> <Text style="text-20 font-600 text-$text-strong">Title</Text> <Button style="px-4 h-9 bg-$accent text-$on-accent hover:opacity-0.85 active:scale-0.97 {saved}:bg-$success" > Save </Button> <Column style="h-2 bg-$accent w-{progress}%" /> </Column> ``` ## 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 <module file>.<target>.<extension>: ### 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<Float>, 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", }; --- <Column style="gap-2"> <Text>{total}</Text> <Text>{files}</Text> <Button on:press={Locale.set("de")}>Deutsch</Button> </Column> ``` ```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; --- <Column style="p-6 gap-3 items-center bg-$surface"> <Text style="text-20 font-600">{label}: {count}</Text> <Text>Doubled: {doubled}</Text> <Button on:press={count += 1}>Add one</Button> #if count > 10 { <Text style="text-$warning">That is a lot of clicks</Text> } </Column> ``` ### 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<Todo>) -> 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<Todo> = []; state draft = ""; state nextId = 1; fn add() { if draft.trim().isEmpty() { return; } todos.push({ id: nextId, title: draft.trim() }); nextId += 1; draft = ""; } --- <Column style="gap-2"> <Row style="gap-2"> <Input style="grow" bind:value={draft} placeholder="New task" on:submit={add} /> <Button on:press={add} disabled={draft.trim().isEmpty()}>Add</Button> </Row> <List items={todos} key={todo => todo.id}> #fill row(todo) { <Toggle bind:checked={todo.done}>{todo.title}</Toggle> } </List> <Text style="text-$text-secondary">{remainingText(todos)}</Text> </Column> ``` ## 18. Decisions for version 1 On 2026-11-04, the project owner decided the open questions of version 1: <!-- https://surface.solenovis.com/docs/components.md --> # 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 <Input on:keydown={key => { if key == "Escape" { editing = false; } }} /> <Input on:submit={add} /> <Button on:press={count += 1}>Add</Button> ``` ### 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 <Text style="text-16"> Run <Span style="font-mono bg-$surface">surf dev</Span>, then read the <Link href="/docs">documents</Link>. </Text> ``` ### 4.4a Span A part of a Text with a style of its own (ADR-0111). The web element is <span>. ### 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 <Image src="assets/logo.png" srcset="assets/logo.png 1x, assets/logo@2x.png 2x" alt="Surface" /> ``` ### 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. ### 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" /> ``` ```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 } ``` ### 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. ### 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. ### 4.6 Button Events: press, doubleclick, focus, blur, keydown. Button accepts text. ### 4.7 Input Events: input (text: String), change (text: String), submit, focus, blur, keydown. ### 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 Events: change (checked: Bool), focus, blur, keydown. Toggle accepts text: the label. ### 4.10 Slider Events: input (value: Float), change (value: Float), focus, blur, keydown. ### 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> ``` ### 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" /> ``` 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. ### 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> ``` Events: change (value: String), focus, blur, keydown. ```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> ``` ### 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> ``` Events: close. ### 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> ``` ### 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. ## 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"; ``` ```surf type UiItem = { value: String, label: String, disabled: Bool = false } ``` ### 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> ``` ### 5.2 Badge A small label for a state or a count. ```surf <Badge tone={.success}>Paid</Badge> ``` ### 5.3 Separator A line between two parts of a page. It has the role separator and an orientation. ```surf <Separator /> <Separator vertical /> ``` ### 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" /> ``` ### 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" /> ``` ### 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" /> ``` ### 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> ``` ### 5.8 RadioGroup A choice of one item. It has the role radiogroup. ```surf <RadioGroup alt="Size" items={sizes} bind:value={size} /> ``` ### 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> ``` ### 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> ``` ### 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> ``` ### 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> ``` ### 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)} /> ``` ### 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> ``` ```surf <Toast bind:open={shown} restart={round} ms={10000}> {notice} #fill action { <Button on:press={undo()}>Undo</Button> } </Toast> ``` ### 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> ``` <!-- https://surface.solenovis.com/docs/layout.md --> # Surface Layout Spec, v0 This document specifies the layout and style model of Surface. It covers the units, the box model, the layout of each built-in component, every style property and the utilities that set them. For each property, it also gives the CSS property (web) and the Taffy 0.15 field (native). ## 1. Conventions ## 2. Values Rules for values: ### 2.1 Theme tokens These are the default tokens. A color token has four values: light, dark, high contrast light and high contrast dark (ADR-0103). The system selects the variant on each target that has the settings: its dark appearance gives the dark value, and its setting for more contrast gives a high contrast value. Theme.setMode also selects it. A length token has the same value in all variants. ### 2.2 Style presets A file style-presets.toml names style text (ADR-0091). It applies to its folder and to the subfolders of that folder. ## 3. Box model ## 4. Built-in components Each built-in component has a fixed layout mode. Styles can change the properties of the mode, but not the mode. ## 5. Property reference The Inh. column says if a child gets the value of its parent when it has no value of its own. All other properties start from their default on each element. ### 5.1 Size ### 5.2 Space A gap value of one length applies to both axes. ### 5.3 Flex container (Column, Row, Scroll, List, Button, Toggle) The flex direction comes from the component (section 4). flex-row and flex-col change it. There are no reverse directions, and they never come (ADR-0129). A reversed row shows the children in an order that is different from the reading order and the focus order. dir-rtl mirrors a row, and an app can reverse its list in code: items.reversed(). ### 5.4 Flex item (a child of a flex container) The utility grow without a value means grow: 1. ### 5.5 Grid container (Grid) A track list is a list of track sizes, separated by spaces. A track size is one of these: ### 5.6 Grid item (a child of a Grid) Line numbers start at 1. Negative line numbers are not valid in v0. ### 5.7 Position and order An absolute element does not take space in its parent. Its position comes from inset, relative to the padding box of the parent. ### 5.8 Visual Rules for visual properties: ### 5.9 Text (Text, and the labels of Button, Toggle, Select) Text shadows (ADR-0145). ### 5.10 Motion The utilities give all properties of one transition the same duration, the same easing and the same delay. Only opacity, transform, background, color, shadow, the color of border and the shape of mask can have a transition. transition names the first six. The shape of mask needs transition-mask, as transition of Tailwind 4 does not name clip-path (ADR-0145). A fade of mask changes at the half of the time, as mask-image in CSS. Colors change on premultiplied sRGB values. A transition of shadow changes its four layers (section 5.8). A layer with no value is then a shadow of size 0 with the color transparent, as in CSS. A gradient and a background image have no transition, as in CSS (ADR-0127). A transition never changes the layout during the animation. All targets follow the rules of CSS for a change during an animation (ADR-0035). A new end value starts at the shown value, and a change back takes as long as the way there. ```surf <Scroll style="h-100vh"> <Text style="animate-rise animate-timeline-view animate-range-entry">Hello</Text> </Scroll> ``` ```surf <Text style="text-48 font-700 split-letters animate-rise">One universal best option</Text> ``` ### 5.11 Paint of shapes (Drawing, ADR-0029) The paint properties change no box. The fill rule is non-zero. A list of dashes with an odd count repeats, as in SVG. ### 5.12 Scroll snapping (Scroll, List and their children, ADR-0128) The rules of CSS apply: ## 6. Layout algorithms The native runtime calls Taffy for one subtree and keeps the results of unchanged subtrees. A change of a style property marks only the element and its ancestors for layout (plan, section 8.1). ## 7. Text measurement A Text element is a leaf. Its measure function answers three questions from the layout engine: ### 7.1 A paragraph of several runs A Text with a Span or a Link in it is one paragraph of several runs (ADR-0111). A run is the text of one element with its style. The native targets use these rules, which are the rules of inline boxes in CSS: ## 8. Pixel snapping ## 9. Paint order ## 10. The web mapping The web emitter writes one stylesheet for each app. The runtime adds no styles of its own. The stylesheet has four parts, in this order: ## 11. Conformance The conformance suite (plan, section 10) checks that the two implementations give the same boxes. ## 12. Utilities A utility is one word in the style attribute of an element. It sets one property of section 5, or one part of a property. The compiler knows every utility. A word that is not a utility is a compile error. ```surf <Row style="items-center gap-3 px-4 py-2 border rounded hover:bg-$surface max-600:gap-1" /> ``` ### 12.1 Values ### 12.2 Variants A variant comes before a utility and ends with :. A utility can have one state and one condition of the table: dark:hover:bg-$surface. {value}: is also a variant (12.5). (...) groups utilities under the same variants: hover:(underline opacity-0.8). ### 12.3 Parts and precedence ### 12.4 Reference A color is a token ($accent), #rgb, #rrggbb, #rrggbbaa, transparent or rgb(<r>_<g>_<b>_/_<a>). ### 12.5 Conditions and case lists ## 13. Decisions for version 1 This section listed the open questions for version 1. On 2026-11-04, the project owner decided them. Version 1 has these features: <!-- https://surface.solenovis.com/docs/standard-library.md --> # Standard library v0 The standard library of version 0 is small: the methods of the built-in types, Int.parse and Float.parse, the built-in names, and the operators. A file uses them without an import. Each method gives the same result on every target. tests/compare.sh compares the results of the native target, the interpreter and the web target. ## 1. Int and Float Int is an integer from −(2^53 − 1) to 2^53 − 1 on all targets. Float is an IEEE 754 binary64 number. ## 2. String A String is a sequence of Unicode scalar values. count, slice and chars count scalar values on all targets, not bytes or UTF-16 units. A string has no index operator. ## 3. List List<T> is an ordered sequence. A literal is [1, 2, 3]. An empty literal needs a known type: state todos: List<Todo> = []. ## 4. Map Map<K, V> maps keys to values. A literal is Map { "a": 1, "b": 2 }. The key type must have the trait Hashable (spec 7.8, ADR-0134): Int, String, Bool, or a record or an enum whose fields all have Hashable. A Float and a dyn type cannot be a key. A type with an impl of Equatable needs an impl of Hashable too. Two keys are one key when their hash is the same and == gives true. ## 4a. Bytes Bytes is an immutable sequence of numbers from 0 to 255 (ADR-0037). == compares the content. A value cannot be a key of a Map, and the markup cannot show it. ## 5. Optional values and results ## 6. Built-in names The theme tokens, for example $accent and $text-secondary, are also built-in names. The layout spec, section 2.1, lists them. ## 6a. Locale The module Locale formats values for a person in the locale of the app (ADR-0132, spec 14). A locale is a BCP 47 tag, for example en, de or pt-BR. ## 7. What the library does not have Version 0 has no functions for these tasks. The right column gives a way to do the task in version 0. <!-- https://surface.solenovis.com/docs/routes.md --> # SurfWave v0 SurfWave gives a project routes from files. A project with a src/routes folder has no src/App.surf. The compiler writes the root component. ## 1. Files A fixed part comes before a parameter, and a parameter comes before a rest. Two pages with one URL are a compile error. surf routes lists the routes. ## 2. Data ```surf // src/routes/tickets/[id]/load.surfs import { AppData } from "../../load.surfs"; import { ticketDetail } from "~/api.surfs"; import { TicketDetail } from "~/model.surfs"; pub type TicketData = { detail: TicketDetail, user: User } pub async fn load(id: String, parent: AppData) -> Result<TicketData, String> { const detail = await ticketDetail(id)?; return .ok({ detail: detail, user: parent.user }); } ``` The desktop target. A desktop app has no server. So it runs each load, layout and later function of load.server.surfs in its own process, as a local call, as it does for a remote function (section 6). It sends no request for the data of a page. ## 3. Props The router gives a page, a layout or an error page the props that it declares: ## 4. The module surf:wave Import it with import { Wave } from "surf:wave". ~/x.surfs is a path from src. ## 5. The server surf build --target server writes build/server/<name>-server. HOST is its address, and PORT is its port. ORIGIN is the address of the site behind a proxy. API_ORIGIN is the http address for a relative Http request, and for /api/. Security lists each setting and each limit of the server. surf build --target static writes each page as a file. ## 6. Remote functions A public async function of a *.remote.surf file runs on the server, and a component calls it as a local function (ADR-0063): ```surf // src/todos.remote.surfs import { Todo } from "./todo.surfs"; pub async fn addTodo(text: String) -> Result<Todo, String> { return .ok(await saveTodo(text)); } ``` ```surf import { addTodo } from "./todos.remote.surfs"; ``` ## 7. Navigation <Link> takes preload="off" or "tap", reload, replace, noScroll and keepFocus. <!-- https://surface.solenovis.com/docs/security.md --> # Security This page lists the limits of the server of surf build --target server, their settings, and what an app must do itself. ## 1. Settings The server reads these environment variables when it starts. A value that is not valid stops the start. ## 2. Fixed limits The server also refuses these requests, and it closes the connection after the answer: ## 3. Run the server behind a proxy The binary has no TLS. A public site needs a proxy in front of it. The proxy must do these tasks: ## 4. What an app must do itself ### Guard each way to the data ### Keep sessions on the server ### Treat each value of a user as hostile ### Keep secrets out of the page ## 5. Tools <!-- https://surface.solenovis.com/docs/commands.md --> # The surf command surf is the only tool that a developer installs. This page describes the install, each command and each option. ## 1. Install The script finds the system, downloads the newest release, checks its SHA-256, and puts surf into ~/.surf/bin. Add this folder to the PATH. The releases cover Linux x86-64 and macOS arm64. ### Build surf from a checkout Only framework developers do this. It needs Rust 1.99 or later: ## 2. Projects ### surf new <folder> Makes a new project from the template. The folder must be empty, or must not exist. The project gets src/App.surf, .gitignore, and surf.toml with the newest edition, for example edition = "2026" (stability). ### surf check <folder> Checks all .surf files in the folder, or in its src folder. The command shows all errors, and it runs no code generator. ### surf lint [folder] Finds code that compiles but is probably wrong (ADR-0070). A finding gives a non-zero exit code. The default folder is the current folder. The rule edition finds a project that names no edition in surf.toml. The rule field-select finds select-none on an Input or a TextArea, because the utility does not turn off the selection of the text of a field (ADR-0131). The rule media-format finds the sources of a Video or an Audio that do not play on each target of ADR-0155. It reads the type of a Source, or the first bytes of a file under assets/. The rules no-captions and autoplay-stop find a Video with sound and no captions, and a Video that plays and loops by itself with no way to stop (ADR-0157, ADR-0158). ### surf test [folder] [--filter <word>] Runs each pub fn test... in the folder tests of the project, with no build, no browser and no window (ADR-0069). Section 8 of the guide lists the functions of surf:test. A test has a clock of its own: Test.wait(ms) moves it, and no real time passes. ### surf fmt [paths...] [--check] Formats .surf files. A folder means its .surf files and the files in its folders. The default path is the current folder. ### surf routes [folder] Shows the routes of a project with src/routes (ADR-0055). ## 3. Builds ### surf build [folder] Builds the project. The default folder is the current folder. #### The systems of a desktop build surf.toml lists the systems that surf build --target desktop and surf package build (ADR-0137): ### surf dev [folder] Serves the project and reloads the page after each change. With --target desktop, it runs the project in a window and reloads it after each change, with the interpreter of the IR. ### surf package [folder] [-o <out>] Builds an installer (ADR-0086). macOS gets a signed disk image, Linux gets a Debian package and a tar file, and Windows gets an MSIX package (ADR-0141). The default output folder is build/package in the project. It builds the systems of the list [desktop] systems, or the one system of --os and --arch (ADR-0137). The files of each system go into build/package/<system>, for example build/package/macos-aarch64/hello-0.1.0.dmg or build/package/windows-x86_64/hello-0.1.0.msix. #### The MSIX package of Windows A .pfx file can sign a test package, but not a release for the public: a public code-signing certificate keeps its key on hardware. The Windows plan (W11) compares the ways to sign a release. ### surf extern stubs [folder] Writes the stubs of the implementation files of the extern modules (language spec, section 13.2): <file>.native.rs and <file>.web.js when they do not exist, and <file>.web.d.ts. ### surf emit-js <folder> Writes the generated JavaScript module to stdout. ### surf emit-rust <folder> Writes the generated Rust code to stdout. ## 4. Packages surf.toml names the packages of a project, and surf.lock holds the commit of each package (ADR-0071). Run these commands in the folder of the project. ### surf add <name> <git or path> [--rev <commit>] Installs a package, and then adds it to surf.toml. When the install fails, surf.toml and src/pkg do not change. The command changes only the package in surf.toml, so the comments and the other lines stay (ADR-0114). ### surf remove <name> Removes a package. ### surf install Installs the packages at the commits of surf.lock. A commit of surf.lock is valid only while the git and the rev of the package in surf.toml stay the same. After a change of these values, the command takes the commit of the new values. ### surf update Installs the newest commit of each package. ## 5. Tools ### surf doctor Checks that this machine has the tools of a build. A release binary reports the private Rust toolchain, the runtime pack and the linker. The first native build downloads the toolchain and the pack, so their absence is not an error. On Linux, the command also reports the libraries of a desktop app. ### surf upgrade Replaces this binary with the newest release after a SHA-256 check. The next native build downloads the runtime pack of the new version, and it removes the old pack and the old build results. A binary from a checkout cannot upgrade. ### surf lsp Runs the language server on stdin and stdout. editors/vscode and editors/zed use it. ### surf mcp [--http <port>] [--bind <address>] [--origin <origin>] Serves the documents of Surface and the checks of the compiler to an agent, through the Model Context Protocol (ADR-0107). ### surf --help Shows the short list of the commands. surf help and -h do the same. ### surf --version Shows the version. -V does the same. ## 6. Environment variables <!-- https://surface.solenovis.com/docs/inspector.md --> # The inspector The inspector shows what a running app does. surf dev starts it, for the web target and for the desktop target. ADR-0105 gives the reasons for this design. ## Open the panel A desktop app has the same panel, in a browser. The default port of a desktop app is 8000. When another program has this port, surf dev chooses a free port and prints it. Use --port to choose a port. ## What the panel shows The sides of a box have the order top, right, bottom, left. A gap has the order row gap, column gap. ### Select an element ### See the redraw regions Press Redraw regions. The app then shows each region for 300 ms. The marks and the regions are not part of the app. They are not in its tree, and a mark is not a redraw region. ### Read a value A value shows as text: ## What it costs A release build does not have the inspector. tests/inspector-release.sh proves this for the web target and for the desktop target. ## The protocol Three parts talk to each other. All text is JSON in UTF-8, and all connections stay on the loopback address. ### Paths Each path starts with /__inspector/. ### Messages to the agent A message has the field method. The broker adds seq, the number of the request. attach and detach come from the broker, and they get no answer. ### Events from the agent The broker also adds reset when an app connects during a session, and when an app goes away. ## Add to the inspector Each change of the protocol changes three files: agent.js, crates/surf-desktop/src/dev/inspector.rs and panel/api.surfs. <!-- https://surface.solenovis.com/docs/ai.md --> # Surface for agents A coding agent knows little about Surf. Surf looks like Svelte, so an agent often writes syntax that does not exist in Surf. Surface gives agents four tools. Each tool works alone, and they work best together. ## 1. Choose a tool ## 2. How the tools help The compiler knows each name of the language. A wrong utility, a wrong method or a wrong attribute is a compile error. So the best tool is the compiler itself: ## 3. Instructions for your project Put this text into the instructions file of your project, for example AGENTS.md or CLAUDE.md: <!-- https://surface.solenovis.com/docs/mcp-server.md --> # MCP server The Model Context Protocol (MCP) is the standard way to give an agent tools. The MCP server of Surface gives the documents of this version, and it checks and formats Surf code. ## 1. Tools, resources and prompt check_code takes an object of file names and source text, for example {"App.surf": "...", "model.surfs": "..."}. A file can also be an .svg icon or style.toml. The server reads no file of your project, and it runs no code of the text. ## 2. Local setup The local server needs the surf command. The surf command has the install. The command is surf mcp, and it uses stdin and stdout. ### Claude Code Run this command. The scope is user, project or local: ### Claude Desktop ### Codex CLI Add the server to ~/.codex/config.toml: ### Cursor ### VS Code The same server in .vscode/mcp.json: ### Zed ### Other clients Use a stdio server with the command surf and the argument mcp. ## 3. Remote setup The address of the hosted server is https://surface.solenovis.com/mcp. It uses the Streamable HTTP transport. ### Claude Code ### Claude Desktop ### Codex CLI Add the server to ~/.codex/config.toml: ### Cursor ### VS Code The same server in .vscode/mcp.json: ### Other clients Read the documents of the client for a remote server, and use the address above. ## 4. Limits of the hosted server The hosted server has no sign-in. So it trusts nothing that a client sends: ## 5. Run your own hosted server surf mcp --http <port> serves the same tools at /mcp: <!-- https://surface.solenovis.com/docs/skills.md --> # Skills A skill is a Markdown file with a name, a description and instructions. An agent reads the description first. It reads the instructions only when a task needs them, so a skill costs little context. ## 1. The skills of Surface The files are in plugins/surface/skills of the repository. This website serves each one: ## 2. Install a skill The plugin installs both skills. To install a skill without the plugin: ## 3. A skill and the MCP server A skill gives rules. It cannot know each name of the language. The MCP server checks each name with the compiler. So use both: the skill tells the agent to call check_code, and check_code finds what the rules miss. <!-- https://surface.solenovis.com/docs/plugins.md --> # Plugins A plugin puts the tools of Surface into an agent with one install. The plugin surface has one source for Claude Code, Codex CLI and Cursor. It is in plugins/surface of the repository. ## 1. Claude Code The marketplace is .claude-plugin/marketplace.json at the root of the repository. Its name is solenovis. ## 2. Codex CLI ## 3. Cursor The plugin has the folder .cursor-plugin for Cursor. Without the plugin, do these two steps: ## 4. Other agents An agent with no plugins can use each part alone: <!-- https://surface.solenovis.com/docs/llms-txt.md --> # llms.txt This website serves its documents as plain text for agents. A file is one request, so an agent with no MCP can read the documents too. ## 1. Choose a file The files and the MCP server have the same text. The MCP server also checks the code, so use it when the agent has MCP. ## 2. The source of the files The build of the website writes the files. It reads the list DOCUMENTS of crates/surf-mcp, which is also the list of the MCP server. So the files, the pages and the server always have the same documents. <!-- https://surface.solenovis.com/docs/targets.md --> # Known differences between the native and the web target The native target paints with its own layout, renderer and text stack. The web target uses the browser. This document lists each known difference that the eye can see. The conformance suite (tests/layout) uses a test font with fixed metrics, so it does not see the differences of text. ## How to compare ## Differences <!-- https://surface.solenovis.com/docs/limits.md --> # Limits of version 0 The specs describe the language that Surface will have. The compiler of version 0 does not have all of it yet. This page lists each gap and each limit of version 0 on one page. The specs mark the same gaps with "Not in version 0". ## 1. Features that are compile errors The compiler rejects these features with an error. The message of most errors ends with "is not supported in version 0". ## 2. Features that behave differently in version 0 ## 3. Checks that version 0 does not do ## 4. Targets The web and the native targets have small differences. A separate page lists them. ## 5. A server ## 6. Tools ## 7. Decided for version 1, not built yet On 2026-11-04, the project owner decided section 13 of the layout spec and section 18 of the language spec (ADR-0124 to ADR-0135). On 2026-11-05, the project owner answered the open questions for version 1 (ADR-0104, ADR-0109, ADR-0113, ADR-0124, ADR-0126, ADR-0134 and ADR-0139 to ADR-0141). On the same day, the project owner decided the motion of version 1 (ADR-0150 to ADR-0154). On the same day, ADR-0155 to ADR-0159 gave the media of P6.25. The specs describe these features, but the compiler of version 0 does not have them yet. Version 0 gives a compile error for the new syntax, or it does what the last column says. <!-- https://surface.solenovis.com/docs/stability.md --> # Stability This page says what an app can rely on after version 1.0. Version 0 has no such promise. ## What stays stable Each release of version 1 keeps these parts: ## What can change ## Editions A change that breaks a project waits for a new edition (ADR-0123). A project names its edition in surf.toml: ## Releases