Control flow
+if / +elseif / +else chains, +each, and +snippet blocks with {@render}.
#+if chains
+if('data.notes.length === 0')
Banner(variant="info") No notes yet
+elseif('data.filtered')
p Showing a filtered list
+else
.space-y-2
+each('data.notes as note (note.id)')
Card {note.title}norns-core rewrites the chain, at any nesting depth, into Svelte blocks emitted as Pug text:
| {#if data.notes.length === 0}
Banner(variant="info") No notes yet
| {:else if data.filtered}
p Showing a filtered list
| {:else}
.space-y-2
...
| {/if}The condition is the string inside the parentheses; quotes are stripped. The rewrite exists because svelte-preprocess's own +if mixin does not support chaining.
#+each
+each('data.notes as note (note.id)')
li {note.title}+each is svelte-preprocess's Pug mixin and its argument is copied verbatim into {#each ...}, so it must be in Svelte's items as item form, optionally with a (key). The item of items form compiles to a block the Svelte compiler rejects; norns lint flags it.
Other svelte-preprocess mixins (+await, +key, +html, +debug) exist upstream but are not part of the vetted subset the framework tests pin; prefer | {#key x} text lines if you need them.
#+snippet
Svelte 5 snippets are how norns-ui components take named content. Define one with +snippet and render it with {@render} behind a pipe:
Header(sticky!="{true}")
+snippet('brand')
a(href="/") Norns
+snippet('nav')
a(href="/docs") Docs
DataTable(rows!="{rows}")
+snippet('cell', row, key)
span {row[key]}+snippet('name') becomes {#snippet name()} and +snippet('row', user, idx) becomes {#snippet row(user, idx)}; the argument list may contain parentheses. Snippets nest, so a Tabs item snippet can contain a Card with its own header snippet.
Inside a component, render what you received:
header(class!="{classes}")
.brand
| {@render brand?.()}
+if('nav')
nav
| {@render nav()}