---
name: surface-best-practices
description: Use this skill when you write Surf code. It gives the rules of the language, the markup and the styles of Surface that differ from TypeScript, Rust, Svelte and Tailwind CSS.
---

# Surf rules

Surface has one way for each task. Use that way. Do not port a pattern from another framework.

## Files

- `src/App.surf` is the root component. A project with routes has `src/routes` in place of it.
- A `.surf` file is a component. Its name starts with an uppercase letter. It has declarations, a line `---`, and markup.
- A `.surfs` file is a module. Its name starts with a lowercase letter. It has no markup.
- `pub` makes a declaration available to other files.
- `~/` names a path from `src`. `./` and `../` name a path from this file.
- `src/style.toml` holds the theme. `style-presets.toml` holds presets for its folder.

## Declarations

```surf
prop label: String = "Clicks";
bindable prop value: Int;
state count = 0;
derived doubled = count * 2;
event changed(by: Int);
slot default;

effect {
    log(count);
}

fn increment() {
    count += 1;
}
```

- `state` is a value that updates the UI. The compiler finds each dependency. Do not write subscriptions, hooks or dependency lists.
- `derived` is a calculated value. You cannot write to it.
- A statement ends with `;`, as in Rust. A form that ends with `}` has no `;`.
- Values are copies, not references. `todos.filter(...)` makes a new list.
- Types: `Int`, `Int64`, `Float`, `Bool`, `String`, `List<T>`, `Map<K, V>`, `Set<T>`, records, enums, `T?`, `Result<T, E>`.
- `Int` has 53 bits on each target.
- `1f64` and `1i64` mark a number type.

## Traits and type parameters

```surf
pub trait Shape {
    fn area(self) -> Float;
}

pub type Circle = { r: Float }

impl Shape for Circle {
    fn area(self) -> Float = 3.14 * self.r * self.r;
}

impl Circle {
    pub fn new(r: Float) -> Circle = { r: r };
}
```

- A trait and an impl block have the form of Rust. Write them only in a `.surfs` file.
- Call an associated function with a dot: `Circle.new(2.0)`. Name an associated type with a dot: `Self.Item`, `T.Item`.
- A file that calls a method of a trait must import the trait.
- A bound is `<T: Comparable + Text>`. `value: impl Text` is the short form for one parameter.
- A trait object is `dyn Shape`, for example `List<dyn Shape>`. Do not write `Box`. Do not compare `dyn` values with `==`.
- A component with type parameters declares them in its script: `component Picker<T: Text>;`. A use can name the type: `<Picker<User> items={users} />`.

## Text for a person

- Use `Locale` for numbers, dates, times, lists and plurals: `Locale.number(1234.5)`, `Locale.date(ms)`. Do not write a format yourself.
- A project that uses `Locale` lists its locales in `surf.toml`: `locales = ["en", "de"]`.
- To sort text in the order of a language, write `names.sortWith((a, b) => Locale.compare(a, b))`.

## Markup

```surf
<Column style="p-6 gap-3">
    #if count > 10 {
        <Text>Many</Text>
    } else {
        <Text>Few</Text>
    }
    #for todo, index in todos key todo.id {
        <Text>{index}: {todo.title}</Text>
    } empty {
        <Text>No tasks.</Text>
    }
    #match load {
        .idle => <Text>Idle</Text>
        .done(count) => <Text>{count}</Text>
    }
    <Input bind:value={draft} on:submit={add} />
    <Button on:press={count += 1}>Add</Button>
</Column>
```

- A block starts with `#`, and a tag starts with `@`.
- A `match` in the script ends each arm with `,`. A `#match` in markup does not.
- `on:press` is the event of a button. It works with a pointer and with a keyboard.
- `bind:` connects an input to a place.
- Give each `#for` over records a `key`.
- An enum case in an attribute is `tone={.warning}`. Do not use text in quotes.
- Use `Column`, `Row`, `Stack`, `Grid`, `Scroll` and `List`. `List` renders only the visible rows, so use it for long lists.
- Use the elements `Text`, `Image`, `Icon`, `Link`, `Button`, `Input`, `TextArea`, `Toggle`, `Slider`, `Select`, `Video` and `Audio`.
- Give a `Video` with sound a `Track` with `kind="captions"`. `surf lint` reports a `Video` with sound and no captions.
- For a word with its own style in a paragraph, use `Span` in the `Text`: `<Text>Run <Span style="font-mono">surf dev</Span> now.</Text>`. A `Link` in a `Text` is a link in the line. Do not put a `Row` of `Text` elements in place of one paragraph.
- A `Span` takes only the utilities of text: color, weight, font, size, italic, underline, line-through and background.
- In a `Text`, a space next to a `Span` stays. A block ends the text, so `#for t in tokens { <Span>{t.text}</Span> }` puts no space between the tokens.
- Give each icon button an `alt`.

## Styles

- All styles are utilities in `style`. There is no CSS file.
- The spacing scale has steps of 0.25 rem. `p-4` is 16 px. `p-4px` is exactly 4 px.
- `w-{x}` is in steps. `w-{x}px` is in px.
- `border-2`, `text-14` and `rounded-8` are exact.
- Use the logical sides: `ps-`, `pe-`, `ms-`, `me-`, `inset-s-`, `inset-e-`, `border-s`, `border-e`, `rounded-s` and `rounded-e`. They mirror in a right-to-left language.
- Use a physical side, for example `pl-` or `left-`, only for a thing that must not mirror. Examples are a map, the axis of a chart and a media control.
- `text-start` and `text-end` align the text. Surf has no `text-left` and no `text-right`.
- A token of the theme always has `$`: `bg-$surface`, `text-$text-secondary`, `p-$gutter`.
- `$` means a token, and nothing else.
- A condition: `{active}:bg-$accent`. A state: `hover:bg-$surface`. A group: `hover:(underline opacity-0.8)`.
- The states are `hover:`, `active:`, `focus-visible:`, `disabled:` and `focus-within:`. Each state can follow `group-` or `peer-`.
- `opacity-`, `scale-`, `brightness-`, `grayscale-` and `leading-` take a factor, not a percentage: `opacity-0.5`, `scale-0.95`, `leading-1.5`. The Tailwind forms `opacity-50`, `scale-95` and `leading-6` are compile errors.
- The utilities of a grid container start with `grid-`: `grid-cols-3`, `grid-rows-2`, `grid-areas-`. The utilities of a stroke start with `stroke-`.
- Text is not selectable by default. Put `select-text` on text that a user can copy. The text of an `Input` and a `TextArea` is always selectable. Do not put `select-none` on a field.
- A gradient is `bg-linear-to-r from-$accent to-$surface`. Its sides are physical, as in Tailwind 4.
- To play animations in an order, declare a `sequence` with a `cue` for each step, and put `cue-<name>` on each element. Do not give each element its own delay for this.
- An enum selects a set: `{tone}:{ warm: bg-$warning, cold: bg-$accent, plain:, }`.
- Breakpoints: `sm:`, `md:`, `lg:`, `xl:`, `2xl:`, `max-md:` and `min-600:`.
- A preset from `style-presets.toml` is `@name`.
- There is no `style:` attribute.

## Before you finish

1. Run `surf check`.
2. Run `surf lint`.
3. Run `surf fmt`.
4. Run `surf test` when the project has tests.
