Language basics
A file is a list of declarations. This chapter covers all fourteen kinds, naming, comments, bodies, strings and the two layouts.
A file is a list of declarations
Every .wf file holds top-level declarations and nothing else — no loose elements, no statements outside a body. There are fourteen kinds:
wf
page Home(path: "/", title: "Home", description: "The front page.") { Text("hi") }
component Chip(_ label: String, tone: Tone = .calm) { Badge(label) }
store Cart { state items = [] derived count = items.length }
theme Brand { color-primary: #0F766E radius-md: 14px }
app { Navbar { Text("Shop").heading } Router }
type Todo { id: String, title: String, done: Bool = false }
enum Tone { calm, loud }
api Backend(base: "/api/v1") { get todos() -> [Todo] }
external Confetti from "https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/+esm" { fn confetti(options: Map) }
const API = "/api/v1"
data posts = "posts.json"
image hero = "hero.jpg"
animation Pulse { from { opacity: 1 } to { opacity: 0.5 } }
test "chip shows its label" { Chip("Beta") expect "Beta" }
| Declaration | What it is | Chapter |
|---|---|---|
|
|
A route and what it shows | |
|
|
A reusable element with typed props, events, slots and parts | |
|
|
State shared across pages | |
|
|
Design tokens: colours, spacing, radii, fonts | |
|
|
The shell every page renders inside, with the |
|
|
|
Records and enumerations the checker enforces | |
|
|
An HTTP service: its address, its endpoints and what they return | |
|
|
A JavaScript module or custom element, typed | |
|
|
A value read everywhere | |
|
|
A JSON file read at build time, as a constant | |
|
|
A picture the build resizes and hands to |
|
|
|
Keyframes | |
|
|
A render held to expectations and a snapshot |
Inside a body there are more statements — state, derived, effect, action, persist, resource, validate, socket, if, for, match and the rest. Grammar lists every keyword and where it may appear.
All the files under src/ merge into one program, so a component declared in components/Chip.wf is usable from every page. App.wf is read first; the rest in name order.
Naming
- Declaration keywords are lowercase:
page,component,store. - The names they declare, and every element, are PascalCase:
Home,UserCard,Button,Table.Row. - State, props, actions, variables and flags are camelCase:
count,isOpen,save,.primary. - Enum cases are written with a leading dot:
.calm,.md. - Design tokens are written with a dollar:
$primary,$spacing-md. - A
///comment above a declaration, a prop or an event is its documentation, shown on hover in the editor and inwf docs.
Nothing resolves silently. A name the compiler does not know — a misspelled element, a flag the element lacks, a case the enum lacks — is an error with a hint, never a no-op.
Comments
wf
// A line comment.
/* A block
comment. */
/// A doc comment: attaches to the declaration that follows it.
component Avatar(_ initials: String) { Text(initials).bold }
Bodies
A declaration's body is a render block: elements, and the statements that shape them (state, derived, if, for, …). A handler or an action's body is an imperative block: statements that do something (assignments, calls, let, if, for, try). The two are separate grammars, and the compiler tells you when a statement is in the wrong kind of block:
wf
page P(path: "/") {
state n = 0
Text("{n}") // an element: fine in a render block
Button("+") {
on click { n = n + 1 } // an assignment: fine in a handler
}
}
Writing n = n + 1 loose inside the page body is an error: "Code that does something goes in on click { … }, an action or an effect".
Statements need no separator. A newline is usual; on one line, a ; or nothing at all works — two spaces is the house style, because it reads:
wf
page P(path: "/") {
state a = 1 state b = 2
Button("x") { on click { a = a + 1; b = b + 1 } }
}
Two layouts: .wf and .wfx
The same grammar can be written with braces or with indentation. A .wf file opens a block with { and closes it with }; a .wfx file opens a block with a line indented deeper than the one before it and closes it with a dedent.
wf
page Home(path: "/") {
state open = true
Row(gap: .sm) {
style {
padding: 6px 0
&:hover { background: $surface-hover }
}
on click { open = !open }
Text("a").bold
}
if open {
Spinner
} else {
Text("b")
}
}
The same in .wfx:
wfx
page Home(path: "/")
state open = true
Row(gap: .sm)
style
padding: 6px 0
&:hover
background: $surface-hover
on click { open = !open }
Text("a").bold
if open
Spinner
else
Text("b")
Rules of the indented layout:
- Four spaces (or one tab) per level, consistently; a line that lines up with no enclosing block is an error.
- Braces you write yourself are still allowed, and everything inside them is free-form:
on click { open = !open }on one line, a map literal across several, an argument list across several. - An empty block is written
{ }. - Blank lines and comment lines count for nothing.
Both layouts compile to the same output, are read by every tool, and a project may mix them. wf fmt --to wfx converts a project one way and --to wf the other, losslessly. This guide writes .wf; every example has an .wfx spelling.
Strings
Strings are double-quoted and interpolate with {expr}:
wf
page P(path: "/") {
state user = { name: "Sam", age: 41 }
Text("Hello, {user.name}! You are {user.age * 12} months old.")
Text("A literal brace: \{ and \}")
Text("A literal dollar: ${user.age}") // `$` alone is plain text
}
Interpolated text is live: when user changes, the text changes. A splice may also say how to show its value — {total:.currency}, {when:.date(long)} — which is formatting written where it is read.
Three ways to write one
A plain "…" string reads escapes (\n, \t, \", \\, \{, \}) and {…} splices. Two other forms exist for text that a plain string makes you spell twice:
wf
// Raw: no escapes, no splices. The text is what is written.
const SAMPLE = #"page Home(path: "/") { state n = 0 }"#
// As many # as the text needs, when it holds a `"#` of its own.
const TRICKY = ##"a raw string ends with "# — like that"##
// Block: over as many lines as it likes, escapes and splices on, and
// the indentation the source gave it removed.
const name = "Ada"
const LETTER = """
Dear {name},
thank you.
""" // "Dear Ada,\n thank you."
A raw string is for a sample of code, a regular expression, a Windows path — anything with quotes or braces in it. A block string is for a paragraph, a template, a query: the closing """ says how far the text was indented, and that much comes off every line.
On this page
A file is a list of declarations Naming Comments Bodies Two layouts: .wf and .wfx StringsChecked by the test suite
Every code block in the guide is parsed, checked and type-checked on each release.