norns-core

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()}