A compact map of what the parser accepts. The chapters say what each form means; this page says where it may be written.
No word is reserved. page, state, type and the rest are keywords only
where a statement starts, so every one of them is also usable as a map key
({ action: "approve", type: "a" }), a field name or a parameter.
Declarations are lowercase; elements and the names declarations introduce
are capitalised; everything else is camelCase.
Where a statement starts, these words have meaning: page, component,
store, app, theme, type, enum, api, const, data, image,
animation and test at the top of a file; state, persist,
derived, effect, cleanup, action, use, resource, validate,
socket, stream, channel, peer, worker, every, after, on key, head,
if, if let, for, show, match, sequence and step in a render
block; slot, event, part and children in a component; let,
return, try, catch, await and emit in an action; expect, click,
type, press in a test. Everywhere else they are ordinary names.
At the top of a file, and nowhere else:
page Name(path: "/", title: …, description: …, image: …, type: …, noindex: …,
layout: Component(…), guard: expr, redirect: "/…", paths: expr,
param: Type, …) { render block }
component Name(_ positional: Type, prop: Type = default, …) { component body }
store Name(scope: .app | .session | .route, eager: Bool) { store body }
app { render block }
theme Name { token: value … }
type Name { field: Type = default, … } type Name = Other { … }
enum Name { case, case(part: Type, …), … }
api Name(base: expr, …) { settings headers { … } on request|response|error(x) { … } endpoints }
api Name from "spec.json" (base: expr)
const NAME: Type = expr
data name: Type = "file.json"
image name = "picture.jpg"
animation Name { from { css } 50% { css } to { css } }
test "name"(data: { … }) { render block and test steps }
A /// comment before a declaration, a prop, a field, an event, an
endpoint, or a state, persist, derived or action of a page, a
component or a store is its documentation.
A page, a component, the app, a test and every element's children:
| Statement | Form |
|---|---|
| state | state name: Type = expr |
| persisted state | persist name: Type = expr { in: .local | .session | .name key: expr version: n sync: Bool migrate n -> n+1 { expr } } |
| derived | derived name = expr |
| effect | effect { imperative cleanup { imperative } } |
| action | action name(param: Type, …) { imperative } |
| store | use Store |
| resource | resource name: Type = fetch(url, options) or = Api.endpoint(args) |
| validation | validate name { rule(args) "message" custom "message" { expr } async "message" { expr } } |
| connection | socket name = ws(url, …) { send T receive T on message(m) { … } on open { … } }, stream name = sse(…), channel name = broadcast(…), peer name = rtc(signal: …, …) |
| timer | every(interval) { imperative }, after(interval) { imperative } |
| key | on key("ctrl+k", e) { imperative } |
| head | head { meta(…) link(…) script(…) } — a page only |
| conditional | if cond { … } else if cond { … } else { … }, if let x = expr { … } |
| loop | for item, index in list by key { … } |
| visibility | show cond { … } |
| choice | match expr { .case(a) { … } loading { … } error(e) { … } ready(v) { … } else { … } } |
| orchestration | sequence { step(after: "120ms") { … } } |
| set-up | Store.action(args) — a call, run once |
| element | Name(positional, prop: value).flag.flag { block } |
| slot use | children, slotName, slotName(value: expr) — a component only |
A component's body is a render block, plus:
slot the default slot, placed with `children`
slot name a named slot
slot name(value: Type, …) a scoped slot
event name(param: Type, …) an event it fires with `emit`
part Name(props) { render block } a part, called as `Component.Name`
In this order, each optional:
Name(…).flags {
style { declarations }
transition { property: duration easing … }
on event(e) { imperative }
slotName(params) { render block } a slot fill, on a component call
children
}
Actions, handlers, effects, timers and hooks:
| Statement | Form |
|---|---|
| assignment | name = expr, a.b = expr, list[i] = expr |
| local | let name: Type = expr, let { a, b } = expr, let [x, y] = expr |
| call | f(args), Store.action(args), list.push(x), navigate("/…"), log(…) |
| emit | emit event(args) — a component only |
| return | return, return expr |
| conditional | if … else …, if let … |
| loop | for x in list { … }, for x, i in list { … }, for n in 1..=10 { … } |
| error handling | try { … } catch e { … } |
| waiting | await expr — makes the action asynchronous |
style {
property: raw css value to the end of the line, a `;`, or two spaces and the next declaration
property: {expr}px $token a live value, a design token
--custom-property: value
&:hover { declarations } nested rules, as CSS nesting writes them
@media (…) { declarations } and `@md { }` for a breakpoint, `@container (…) { }`
}
A theme's body is the same declarations, without nesting.
In a test, beside render statements: expect "text", expect not "text",
click "name", type "text" into "name", press "Key",
press "Key" in "name".
From weakest to strongest binding:
| Level | Operators |
|---|---|
| 1 | .. ..= (ranges) |
| 2 | ?? |
| 3 | || |
| 4 | && |
| 5 | == != (!== too) |
| 6 | < <= > >= |
| 7 | + - |
| 8 | * / % |
| 9 | unary ! -, await |
| 10 | calls f(…), fields a.b, a?.b, indexes a[i], a?.[i], flags |
So 1..n + 1 is 1..(n + 1), and a ?? b || c is a ?? (b || c).
Values that are expressions: if c { a } else { b } (null when there is no else and c fails), if let x = v { a } else { b },
match v { .case { a } else { b } }, lambdas x => expr, (a, b) => expr,
() => expr, spreads ...x in a list or map.
| Kind | Written |
|---|---|
| String | "text {splice} {value:.style(option)}", raw #"…"#, block """…""" |
| Number | 42, 3.14 |
| Bool, null | true, false, null |
| List, map | [a, b], { key: value, "Quoted-Key": value, ...other } |
| Regex | /pattern/flags |
| Date, time | @2026-03-14, @09:30, @2026-03-14T09:30Z |
| Duration | 250.ms, 5.seconds, 90.minutes, 2.hours, 3.days, 1.weeks |
| Money | €12.99, $9.99, £4.50 |
| Colour | #0F766E |
| Enum case | .name, .name(payload) |
| Design token | $name |
// to the end of the line, /* across lines */, and /// documentation.
A .wf file writes blocks in braces. A .wfx file writes them by
indentation: a line indented deeper than the one before opens a block, a
dedent closes it, and braces written by hand still work inside a line
(chapter 5). Statements need
no separator; on one line, a ; or two spaces reads best.