Docs

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

DirectivePurpose
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:fadeenter/leave animation (slide, scale too)
animate:flipFLIP animation on keyed rows
use:actionrun 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.