# 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;

---
<Column style="p-6 gap-3">
    <Text>Count: {count}</Text>
    <Text>Doubled: {doubled}</Text>
    <Button on:press={count += 1}>Add one</Button>
</Column>
```

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<Item>) -> 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);

---
<Row style="gap-2 items-center">
    <Toggle style="grow" bind:checked={item.done}>{item.name}</Toggle>
    <Button on:press={emit remove(item.id)}>Remove</Button>
</Row>
```

- `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<Item> = [];
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);
}

---
<Column style="p-6 gap-3 max-w-120">
    <Text style="text-24 font-600">Groceries</Text>
    <Row style="gap-2">
        <Input style="grow" bind:value={draft} placeholder="Milk, bread, ..." on:submit={add} />
        <Button on:press={add} disabled={draft.trim().isEmpty()}>Add</Button>
    </Row>
    #for item in items key item.id {
        <ItemRow bind:item={item} on:remove={remove} />
    } empty {
        <Text style="text-$text-secondary">The list is empty.</Text>
    }
    <Text style="text-$text-secondary">{left(items)}</Text>
</Column>
```

- `state items: List<Item> = []` 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
<Button
    style="px-4 py-2 rounded-8 bg-$accent text-$on-accent font-600 hover:bg-$accent-hover
           focus-visible:ring-2 focus-visible:ring-$focus disabled:opacity-50 transition-background
           duration-200"
>
    Save
</Button>
```

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
   <Input testid="draft" style="grow" bind:value={draft} placeholder="Milk, bread, ..." on:submit={add} />
   <Button testid="add" on:press={add} disabled={draft.trim().isEmpty()}>Add</Button>
   <Text testid="left" style="text-$text-secondary">{left(items)}</Text>
   ```

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/<system>`. 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 `<title>` 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.
