Components
Build Niral components with server blocks, scripts, templates and scoped CSS. Learn props, slots, events, bindings and JSX or TSX without React.
On this page 5 sections
Components are .niral files — or .jsx / .tsx if you prefer JSX. Same compiler, same runtime, no React.
Blocks
<server> … server-only code (any language) … </server>
<script> … client logic with runes … </script>
<head> <title>{title}</title> </head>
<style> … scoped CSS … </style>
… template …
Template syntax
<p>{expression}</p>
{#if user}
<b>Hi {user.name}</b>
{:else if pending}
<span>loading…</span>
{:else}
<a href="/auth/login">sign in</a>
{/if}
{#for item, i of items key item.id}
<li>{i}: {item.text}</li>
{/for}
{#await promise}
<p>loading…</p>
{:then value}
<p>{value}</p>
{:catch err}
<p>{err.message}</p>
{/await}
{@html trustedHtml}
Keyed {#for} does real reconciliation — DOM nodes, input state and effects survive reorders, and animate:flip makes rows glide.
Directives
| Directive | Purpose |
|---|---|
on:click={fn} | events (any DOM event) |
bind:value={name} | two-way input binding — paths work too: bind:value={todo.text} |
class:active={cond} | toggle one class |
style:color={c} | one reactive style property |
transition:fade | enter/leave animation (slide, scale too) |
animate:flip | FLIP animation on keyed rows |
use:action | run a function with the element (client-only) |
Composition
<script>
import Card from "../components/Card.niral"
let n = $state(0)
</script>
<Card title="Stats" tone="accent" on:save={() => n++}>
clicked {n} times <!-- slot content -->
</Card>
Inside Card.niral, <slot/> renders the children and on:save arrives as an onSave prop. Props update fine-grained — the child keeps its local state.
Named slots and fallbacks
Card.niral can expose multiple outlets:
<header><slot name="header">Untitled</slot></header>
<main><slot>No content yet</slot></main>
<footer><slot name="footer"/></footer>
The parent assigns direct children to those outlets:
<Card>
<h2 slot="header">Account</h2>
<p>Account details</p>
<template slot="footer">
<button on:click={save}>Save</button>
<button on:click={cancel}>Cancel</button>
</template>
</Card>
Names must be static, non-empty strings. Unassigned children use the default slot. A template group adds no DOM wrapper, and assignment attributes are not rendered. Fallback content renders only when the caller supplies no content for that outlet; an explicitly empty group suppresses it. Put conditional named content inside a template slot="name" group. The same rules work in JSX/TSX, SSR and hydration.
Two-way component bindings
A parent explicitly grants write access with bind::
<script>
import Editor from "../components/Editor.niral"
let name = $state("")
</script>
<Editor bind:value={name}/>
<p>{name}</p>
Inside Editor.niral, use the prop directly:
<script>
let { value = "" } = $props
</script>
<input bind:value={value}/>
Updates flow in both directions without recreating the child. Ordinary props remain read-only; no child-side rune is required. Wrappers can forward a binding with <Editor bind:value={value}/>. State property paths such as bind:value={form.name} and bind:value={form[field]} also work. Derived values, plain variables and arbitrary expressions are rejected as binding targets. Forwarding a prop that the caller did not bind remains read-only.
Lifecycle and cleanup
onMount and onDestroy are available in component scripts without imports:
<script>
let seconds = $state(0)
onMount(() => {
const timer = setInterval(() => seconds++, 1000)
return () => clearInterval(timer)
})
</script>
<p>{seconds} seconds</p>
onMount runs in a microtask after DOM attachment or hydration, never during SSR. Return a cleanup function from a synchronous callback to release resources on unmount. Removing a component before that microtask cancels its mount callback. onDestroy(() => cleanup()) registers setup-time cleanup, including during SSR. Cleanup follows ownership through conditional branches and keyed rows, runs once, and also runs if component setup fails. Runtime effect callbacks may return a cleanup function; it runs before the next execution and on disposal.
JSX / TSX
export default function Todo({ items }) {
let draft = $state("")
return (
<ul>
{items.map((t) => <li key={t.id}>{t.text}</li>)}
</ul>
)
}
.tsx files are type-stripped at compile time and checked by niral check.