docs / Runtime

Headless Components

Show, For, Switch and Match — composable render helpers with no markup and no styling.

Vesk provides headless render helpers — `Show`, `For`, `Switch`, `Match` — for conditional rendering, list rendering, and pattern matching. They are composable building blocks with no markup and no styling, and they are **auto-imported** from `@vesk/runtime` (no import statement needed).

Why headless components exist alongside native statements

Vesk has two rendering systems that overlap in purpose but differ in where they can appear. Understanding the difference is the key to picking the right one.

**Native statements: `if`, `for`, `switch`**

The compiler lowers statement-mode `if`, `for...of`, `switch`, and `while` directly into reactive IR. They are always available — no import, no helper, no ceremony. They are the **default** way to render in a component body.

  • `if (cond) return <p>Loading</p>;` — guard-clause early return, idiomatic for `useFetch` loading/error guards.
  • `for (const item of items; key item.id) { <li>{item}</li> }` — keyed list rendering that compiles to efficient DOM updates.
  • `switch (status) { case 'active': return <span>Active</span>; ... }` — multi-way branching with strict equality.

These work **only in statement position** — the top of a component body, inside a block, or as a guard-clause return. They cannot appear inside a `return (...)` expression, inside `{...}` interpolation, or as arguments to `.map()`.

**Headless components: `Show`, `For`, `Switch`, `Match`**

Headless components are JSX tags. They work **wherever a JSX tag works** — inside `return (...)`, inside `{...}` interpolation, inside `.map()` callbacks, as children of other components, or as standalone statement-mode JSX. They are values, not control flow.

  • `<Show when={cond}>...</Show>` — the expression-position `if`. Useful inside a `return (...)`, inside a `map`, or anywhere you need a conditional that is itself just a value.
  • `<Switch><Match when=.../>...</Switch>` — the expression-position `switch`. Stays flat where nested ternaries would indent. Multiple `<Match>` arms, with `<Match fallback>` for the default.
  • `<For each={items}>` — the expression-position list render. Takes a `children` render function. In `.vsk`, prefer the native `for...of ; key` loop instead (see below).

**When to pick which**

Scenario Use native statement Use headless component
Guard clause at top of body `if (loading) return <Spinner/>;` —
Conditional inside `return (...)` — `return (<Show when={user}>{user.name}</Show>)`
Conditional inside a `map` — `items.map(i => <Show when={i.active}>{i.name}</Show>)`
Keyed list, statement body `for (const x of items; key x.id) { ... }` —
List inside `return (...)` — `return (<ul>{items.map(...)}</ul>)`
Multi-way, statement body `switch (x) { case 'a': ... }` —
Multi-way inside JSX — `<Switch><Match when=...>...</Match>...</Switch>`

Both forms compile to the same rendering. Pick whichever reads more naturally in the surrounding code.

Show

`Show` renders its children when `when` is truthy, otherwise its `fallback`. The `fallback` is a string, a JSX element, or omitted (`null`).

app/components/Greeting.vsk
										
											component Greeting(props: { name?: string }) {
  <Show when={props.name} fallback="Hello, stranger">
    <p>Hello, {props.name}</p>
  </Show>
}
										
									

For

`For` takes an `each` list and a `children` render function `(item, index) => vnode`. In `.vsk`, JSX render-function children (`{(item, i) => ...}` directly inside the tag) do **not** compile — prefer the native `for...of ; key` loop for list rendering. The `for...of` form compiles to keyed reconciliation and reads like plain JavaScript.

app/components/Greeting.vsk
										
											component Greeting(props: { name?: string }) {
  <Show when={props.name} fallback="Hello, stranger">
    <p>Hello, {props.name}</p>
  </Show>
}
										
									

The `; key` clause you see in the statement-mode example is a Vesk extension and is covered on the List Rendering page. It compiles to keyed DOM reconciliation — adding, removing, and reordering elements without re-rendering the whole list.

Switch / Match

`<Switch>` returns the first child that renders non-empty; `<Match when=...>` renders its children only when `when` is truthy, and a `<Match fallback>` matches the default arm. This is the expression-position analogue of a `switch` with `default`, useful when a ternary would nest.

app/components/Greeting.vsk
										
											component Greeting(props: { name?: string }) {
  <Show when={props.name} fallback="Hello, stranger">
    <p>Hello, {props.name}</p>
  </Show>
}