# Nørd :::Tip Want to get started? Skip to [Getting started](/getting-started). ::: Nørd is a small reactive framework for building web interfaces with JavaScript or TypeScript. It stays close to the platform: ordinary functions compose interfaces, tagged templates create DOM fragments, and reactive values update only the DOM locations that use them. ## Tenets - **Stay close to JavaScript.** Components are functions, templates use tagged template literals instead of JSX, and state can live in ordinary closures or modules. - **Keep the runtime small.** Nørd has no third-party runtime dependencies, compiler, JSX transform, framework plugin, or special file format. - **Update the DOM directly.** Reactive changes update their bound nodes without re-running components, using a virtual DOM, or diffing a tree. - **Use the browser platform.** HTML, CSS, events, and browser APIs remain available, with directives for controlled DOM access. - **Do not prescribe architecture.** Nørd works in a standalone page, an existing site, or a larger application without imposing a file or state structure. - **Render static HTML where it fits.** Server rendering supports static output and independently activated islands rather than stateful incremental hydration. ## Core concepts - **Templates and components** use `html` tagged templates and ordinary functions to create fragments. - **Reactive values** such as Grains subscribe DOM bindings. Any compatible value with a `subscribe` method can be used instead. - **Directives** attach behavior to one element: events, attributes, refs, lifecycle work, and browser integrations. - **Structs** own dynamic regions of the DOM for conditions, lists, promises, and other multi-node updates. - **Rendering** mounts a fragment in the browser or renders it to an HTML string on the server. ## Why Nørd? Nørd is a good fit for applications that benefit from reactive UI without taking on a compiler pipeline or a broad framework runtime. It can be dropped into an existing page, used for independently activated islands, or used to build a complete client application. The focus is a compact, readable API that leaves the browser platform and application architecture in view. ## Performance trade-offs Nørd is optimized for fine-grained updates: once a fragment exists, a reactive value changes only the DOM location that uses it. There is no component re-run, virtual-DOM diff, or replacement of the surrounding tree. The complementary trade-off is initial bulk creation. Templates are parsed at runtime rather than compiled ahead of time, so creating a very large number of rows is not Nørd's fastest workload. Updating existing rows surgically is where it performs best. - [JS Benchmarks results](https://jsbenchmarks.com/) are available now. - [js-framework-benchmark results](https://krausest.github.io/js-framework-benchmark/) are also available. Benchmark results vary with the browser, hardware, and methodology. Treat them as workload-specific evidence rather than a universal framework ranking. ## Ecosystem Nørd's core is deliberately small. Companion packages add focused capabilities without becoming part of the runtime: - `@grainular/forms` provides reactive controls, validation, bindings, and error rendering. - `@grainular/router` provides client-side routing, navigation, outlets, hooks, and transitions. - `@grainular/resource` provides abortable reactive asynchronous resources. - `@grainular/silo` provides small selector-based stores. - `@grainular/styled` provides scoped component styles. - `@grainular/custom-elements` exposes Nørd components as platform custom elements. - `@grainular/aurora` builds static documentation and content sites with optional independent islands. These packages have their own APIs and should not be inferred from React, Vue, or other ecosystem conventions. This guide deliberately documents Nørd core and Grains rather than companion-package APIs. Nørd does not currently provide an official i18n package; use the platform `Intl` APIs or an application-level library. ## Examples These examples are a quick tour, not the whole documentation. The dedicated guides explain each API in detail. ### Hello World This is all that is required to render a fragment into a page: ```ts title="app.ts" import { html, mount } from '@grainular/nord'; const App = () => html`Hello World`; mount(App, { to: document.querySelector('#app') }); // Or, equivalently: // mount(() => html`Hello World`, { to: document.querySelector('#app') }); ``` ### Components are just functions Components return fragments. Passing props and composing components is just calling functions with ordinary objects. ```ts title="app.ts" import { html } from '@grainular/nord'; const Greeting = ({ name }: { name: string }) => html`

Hej, ${name}!

`; export const App = () => html`
${Greeting({ name: 'Nørd' })}
`; ``` [Templates and components](/templates-and-components) covers props, children, and composition. ### Component functions only run once Nørd evaluates a component when its fragment is created, then maintains the reactive bindings and structs inside that fragment. It does not call the component again for every update. ```ts title="app.ts" import { html } from '@grainular/nord'; export const App = () => { console.log('Created once'); return html`

This fragment can still contain reactive values.

`; }; ``` ### Grains are not scoped — unless you scope them State can be local to a component, shared from a module, or passed through props. A grain has no special scope of its own; it updates wherever it is used. ```ts title="counter.ts" import { grain } from '@grainular/grains'; import { html, on } from '@grainular/nord'; export const count = grain(0); export const Counter = () => html` `; ``` [Reactivity and grains](/reactivity) explains local state, shared state, derived values, and other subscribable sources. ### Comments are just HTML comments Templates use HTML syntax. Nothing special is required for a comment: ```ts title="app.ts" import { html } from '@grainular/nord'; export const App = () => html`
`; ``` ### Directives access the DOM Directives are small functions placed on an element. The built-in `on` directive attaches an event listener and cleans it up when the element leaves the document. ```ts title="button.ts" import { html, on } from '@grainular/nord'; export const Button = () => html` `; ``` [Directives](/directives) cover events, attributes, refs, portals, and custom directives. ### Structs handle control flow Structs manage dynamic regions rather than a property on a single element. `$if` replaces its region when the condition changes. ```ts title="status.ts" import { grain } from '@grainular/grains'; import { $if, html } from '@grainular/nord'; const isReady = grain(true); export const Status = () => html` ${$if(isReady) .$then(() => html`Ready`) .$else(() => html`Not ready`)} `; ``` ### Async is not an issue Promises can have pending, resolved, and error fragments without introducing a separate component type. ```ts title="profile.ts" import { $await, html } from '@grainular/nord'; const profile = fetch('/api/profile').then((response) => response.json()); export const Profile = () => html` ${$await(profile) .$then((user) => html`

Hej, ${user.name}!

`) .$pending(() => html`

Loading...

`) .$catch((error) => html`

${error.message}

`)} `; ``` ### Lists are the final boss `$each` reconciles a reactive iterable and uses keys to preserve each item’s DOM identity. ```ts title="users.ts" import { grain } from '@grainular/grains'; import { $each, html } from '@grainular/nord'; const users = grain([ { id: 'ada', name: 'Ada' }, { id: 'lin', name: 'Lin' }, ]); export const Users = () => html` `; ``` ### If Nørd does not have what is needed, build it `createDirective` and `createStruct` expose the same extension points used by the built-in primitives. ```ts title="color.ts" import { createDirective, html } from '@grainular/nord'; const color = (value: string) => createDirective((node) => { node.style.backgroundColor = value; }); export const App = () => html`

Custom directive.

`; ``` [Structs](/structs) cover dynamic regions, including lists and asynchronous values. Custom directives and structs each have their own guide. ### Rendering HTML The same component model can produce static HTML on the server: ```ts title="server.ts" import { renderToString } from '@grainular/nord'; import { App } from './app'; const html = renderToString(App); ``` [Server rendering](/server-rendering) explains static generation and independent islands. ## Next steps [Getting started](/getting-started) covers installation and project setup. The following guides expand each concept without requiring a particular application architecture or tooling choice. --- # Getting started :::Tip Want to know more about Nørd? Start with the [overview](/). ::: :::Tip You can try out Nørd directly in the browser on [Stackblitz](https://stackblitz.com/edit/grainular-nord-vite-ts?file=src%2Fapp.ts) ::: ## Installation The official scaffolder is the quickest way to start a project. Run one of the following commands and follow the prompts: :::CodeGroup{label="Create a Nørd project"} ```bash title="npm" npm create @grainular/nord ``` ```bash title="pnpm" pnpm create @grainular/nord ``` ```bash title="Bun" bun create @grainular/nord ``` ::: The scaffolder selects a project directory and template interactively. The Vite TypeScript template is a sensible default for most applications. The browser template is useful when no package installation or build process is needed. Vite is only used for development and bundling. Nørd does not need a Vite plugin. For an existing project, install Nørd and Grains directly: :::CodeGroup{label="Install manually"} ```bash title="npm" npm install @grainular/nord @grainular/grains ``` ```bash title="pnpm" pnpm add @grainular/nord @grainular/grains ``` ```bash title="Bun" bun add @grainular/nord @grainular/grains ``` ::: `@grainular/nord` contains the renderer and its core APIs. `@grainular/grains` provides the reactive values used throughout this guide. Grains is optional: Nørd can work with any compatible subscribable value. ## Using Nørd from a CDN Nørd also works without a package manager or build step. Its ESM bundle can be imported from a CDN in an ordinary HTML file: ```html title="index.html" My Nørd application
``` These are native browser modules, so they do not add anything to the global scope. Serve the file with any static HTTP server and open it in a modern browser. Code running directly in the browser must remain regular JavaScript; TypeScript-only syntax is not supported there. ## Project setup The Vite TypeScript template starts with a small project structure: ```text my-app/ ├── index.html ├── package.json ├── vite.config.ts └── src/ ├── app.ts ├── app.css ├── main.ts └── style.css ``` `index.html` contains the application root, `main.ts` mounts the application, and `app.ts` contains the first component. This is a starting point rather than a required convention; components, state, directives, and styles can follow the structure of the application. Start the development server with the command for the selected package manager: :::CodeGroup{label="Run the project"} ```bash title="npm" npm run dev ``` ```bash title="pnpm" pnpm dev ``` ```bash title="Bun" bun run dev ``` ::: Use the matching `build` command to create production assets and `preview` to inspect them locally. These commands belong to Vite; Nørd itself does not add another build layer. ## Next steps With the project set up, continue with [Templates and components](/templates-and-components). [Reactivity and grains](/reactivity) introduces reactive state, [Directives](/directives) cover element behavior, and [Structs](/structs) handle dynamic regions. --- # Templates and components Templates describe DOM fragments, while components organize them with ordinary functions. ## HTML templates Nørd uses the `html` tagged template to create DOM fragments. The literal parts are regular HTML, so elements, attributes, comments, and browser semantics remain visible in the component source. There is no JSX transform or separate template file to configure. ```ts title="welcome.ts" import { html } from '@grainular/nord'; const name = 'Nørd'; export const Welcome = () => html`

Hej, ${name}!

This is an ordinary HTML template literal.

`; ``` Interpolations can contain primitive values, subscribable values, component fragments, directives, or structs. Nørd places the appropriate fragment at that location, whether it belongs in text content, an attribute, or a dynamic DOM region. :::Tip{title="Fragments are ordinary values"} Fragments have no special context or ownership. Declare them wherever it makes sense, pass them through functions or props, store them in variables, and render them later. A fragment becomes DOM only where you interpolate it. ::: ## Components Components are functions that return the `ComponentFragment` created by `html`. They are evaluated when the surrounding fragment is created, not repeatedly on every state change. Reactive bindings and structs inside the resulting fragment own subsequent updates. ```ts title="greeting.ts" import { html } from '@grainular/nord'; export const Greeting = () => html`

Hej!

`; ``` `ComponentFragment` and `PureComponent` are useful types when a signature benefits from being explicit, but neither is required for Nørd to recognize a component. A function returning `html` is enough. ## Component props Props are ordinary objects passed to a component function. There is no compiler transform, special argument position, or reactive-prop wrapper: values and functions are passed exactly as they would be to any other JavaScript function. ```ts title="greeting.ts" import { html, type PureComponent } from '@grainular/nord'; type GreetingProps = { name: string; punctuation?: string }; export const Greeting: PureComponent = ({ name, punctuation = '!' }) => html`

Hej, ${name}${punctuation}

`; ``` The component is then called in a template with its props object: `${Greeting({ name: 'Nørd' })}`. A prop can itself be a grain, callback, component fragment, or any other value required by the component. ## Children Children are props with a familiar name, not a separate syntax feature. `PropsWithChildren` adds a `children` field to a component’s own props type; callers provide the child string or fragment explicitly. ```ts title="card.ts" import { html, type PropsWithChildren } from '@grainular/nord'; type CardProps = PropsWithChildren<{ title: string }>; export const Card = ({ title, children }: CardProps) => html`

${title}

${children}
`; const article = Card({ title: 'A card', children: html`

Its content is a normal fragment.

`, }); ``` This keeps component APIs explicit. A component can call its prop `children`, use a different prop name, accept several content fragments, or not accept child content at all. ## Composition Components compose by calling other components in a template. Because the child has already returned a fragment, there is no component instance object or implicit lifecycle hierarchy to manage. ```ts title="page.ts" import { html } from '@grainular/nord'; import { Card } from './card'; const Header = () => html`
Nørd
`; export const Page = () => html` ${Header()}
${Card({ title: 'Composition', children: html`

Functions all the way down.

` })}
`; ``` Local state belongs wherever the function is called. Put a grain inside a component for a fresh value per component fragment, in a module for shared state, or pass it through props when ownership should remain with the parent. ## Reactive values in templates When a subscribable value is interpolated into a template, Nørd creates a direct binding to that DOM location. Updating the value changes the text or attribute binding in place; it does not call the component again or recreate its surrounding elements. ```ts title="counter.ts" import { grain } from '@grainular/grains'; import { html, on } from '@grainular/nord'; export const Counter = () => { const count = grain(0); return html` `; }; ``` ## Attributes Attribute interpolation works exactly like text interpolation: pass a subscribable directly and Nørd keeps the attribute synchronized. This is often clearer than reaching for a directive. ```ts title="toggle.ts" const enabled = grain(false); const Toggle = () => html` `; ``` Standard HTML boolean attributes follow HTML presence semantics. A truthy `disabled`, `checked`, `hidden`, or `required` binding renders the empty attribute; a falsy value removes it. Other attributes, including ARIA attributes, are serialized as ordinary strings, so `aria-pressed` becomes `"true"` or `"false"`. Grains are the default reactive values in these docs, but Nørd accepts any compatible subscribable. Continue with [Reactivity and grains](/reactivity) for writable state, derived values, and integrating other reactive sources. ## Inline template logic As templates are evaluated only once, logic executed within template interpolation can be used to render static segments. For example, a static array can be iterated directly inside the template, without the need for `$each`. ```ts const List = () => { const items = [1, 2, 3]; return html``; }; ``` The same is possible for conditional interpolation, or any other JavaScript logic directly in the template. ## Familiar patterns that do not apply Nørd resembles the platform more than component frameworks with compiler transforms or render loops. Do not carry these patterns across unchanged: - **Do not interpolate a getter:** `${() => count()}` is not a reactive binding. Interpolate the subscribable itself: `${count}`. - **Do not use `onClick`, `@click`, or similar template attributes for events.** They are not event bindings. Use a directive: `${on('click', handler)}`. - **Do not interpolate a component function without calling it.** Render `${Counter()}`, not `${Counter}`. - **Do not expect a component to run again when state changes.** Components create fragments once; interpolate a subscribable or use a control-flow struct for the part that must update. - **Do not use a virtual-DOM mental model.** Nørd updates a bound node directly, and structs own their explicit DOM regions. --- # Reactivity and grains Nørd updates individual DOM bindings when their subscribed values change. Components do not run again, and the surrounding DOM stays in place. ## Subscribable values Nørd does not require a particular state library. A reactive value only needs to be a callable getter with a `subscribe` method that returns an unsubscribe function. ```ts title="subscribable.ts" type Subscribable = { (): T; subscribe(listener: (value: T) => void): () => void; }; ``` Interpolating a subscribable creates a binding at that exact location. When it changes, Nørd updates the corresponding text node or attribute and disposes of the subscription with the owning DOM node. :::Tip{title="Reactivity is explicit"} Nørd does not collect dependencies while a component runs. Pass a subscribable to the DOM location that should update, or use a struct when a value controls a region of DOM. ::: ## Creating grains `grain` is the small writable primitive provided by `@grainular/grains`. A grain is a function: call it to read its current value. ```ts title="count.ts" import { grain } from '@grainular/grains'; const count = grain(0); count(); // 0 ``` Grains are synchronous closures. They hold no global context, can be declared in any module or component, and can be passed like any other value. By default, a grain notifies subscribers when its next value is not `Object.is` equal to the current one. Pass a comparison function when a value needs a different update boundary. Return `true` when both values should be treated as equal. ```ts title="user.ts" const user = grain(initialUser, (current, next) => current.id === next.id); ``` Use a custom comparison deliberately: it decides whether Nørd receives an update at all. ## Updating writable grains Writable grains provide `set` for a replacement value and `update` when the next value depends on the current one. ```ts title="count.ts" count.set(3); count.update((value) => value + 1); count(); // 4 ``` Updates notify subscribers synchronously when the value changes according to `Object.is`. There is no implicit batching or scheduled component render. ## Readonly state `readonly` exposes the readable, subscribable side of a writable grain without its `set` and `update` methods. It is useful when a module owns state but should not let consumers change it directly. ```ts title="session.ts" import { grain, readonly } from '@grainular/grains'; const session = grain(null); export const currentSession = readonly(session); export const signIn = (next: Session) => session.set(next); export const signOut = () => session.set(null); ``` `currentSession` can still be read, subscribed to, or interpolated in a template. The writable grain remains private to the module that owns it. ## Derived values `derived` creates a readonly subscribable by mapping another grain. Use `combined` first when a value depends on several sources. ```ts title="total.ts" import { combined, derived, grain } from '@grainular/grains'; const quantity = grain(2); const unitPrice = grain(12); const invoice = combined([quantity, unitPrice]); const total = derived(invoice, ([quantity, price]) => quantity * price); total(); // 24 ``` Derived values are readonly; update their source grains instead. ## Composing reactive state `combined` joins several grains into one readonly tuple, which is useful when one binding depends on several sources. `flattened` unwraps a grain that contains another grain and follows whichever inner grain is currently selected. ```ts title="selection.ts" import { flattened, grain } from '@grainular/grains'; const primary = grain('Primary'); const secondary = grain('Secondary'); const selected = grain(primary); const label = flattened(selected); label(); // 'Primary' selected.set(secondary); label(); // 'Secondary' ``` Subscribers to `label` are moved from the previous inner grain to the next one automatically. This is useful for active selections, replaceable stores, and any state that chooses another reactive value. ## Local and shared state Where a grain is declared determines who owns it. A grain inside a component function is created for each component fragment. A grain in module scope is shared by every consumer of that module. ```ts title="counter.ts" import { grain } from '@grainular/grains'; import { html } from '@grainular/nord'; const sharedCount = grain(0); export const LocalCounter = () => { const localCount = grain(0); return html`

${localCount} / ${sharedCount}

`; }; ``` Neither choice needs a provider or special scope. Pass a grain through props when the parent should retain ownership; keep it local when the component owns the state. ## Using another reactive source If another reactive source already has a synchronous getter and subscription API, wrap it with `syncReactive`. The resulting readonly subscribable works anywhere a grain does. ```ts title="store.ts" import { syncReactive } from '@grainular/nord'; const currentUser = syncReactive({ get: () => store.user, subscribe: (notify) => store.subscribe(notify), }); const Profile = () => html`

${currentUser}

`; ``` The wrapper subscribes to the external source only while Nørd has active consumers, then releases it automatically. Keep external adaptation at the application boundary; components can remain unaware of the state library behind a subscribable. --- # Lifecycle and cleanup Nørd ties registered cleanup to the lifetime of rendered DOM nodes. There is no component instance lifecycle: behavior belongs to the element or dynamic region that owns it. ## Mounting an application `mount` renders a component into an element or document fragment. It creates the application DOM off-screen, hydrates its bindings, then replaces the target’s existing children in one step. ```ts title="main.ts" import { mount } from '@grainular/nord'; import { App } from './app'; mount(App, { to: document.querySelector('#app') }); ``` The target is the application boundary. Nørd observes it for DOM lifetime changes and uses it as the root for registered cleanup. ## Component evaluation Components are ordinary functions evaluated while their surrounding fragment is created. Changing a grain does not call the component again; the reactive fragments, directives, and structs created during that initial evaluation own later DOM updates. ```ts title="clock.ts" import { grain } from '@grainular/grains'; import { html } from '@grainular/nord'; export const Clock = () => { const now = grain(new Date()); return html``; }; ``` Put setup that should run once per rendered fragment in the component function. Put DOM-dependent work in a directive or `mounted` callback instead. ## Mounted behavior `mounted` runs after its host element is connected to the document. Return a function to clean up work when that element is removed. ```ts title="autofocus.ts" import { html, mounted } from '@grainular/nord'; const autofocus = mounted((node) => { const input = node as HTMLInputElement; input.focus(); return () => input.blur(); }); export const Search = () => html``; ``` This is the right place for DOM APIs that require a connected element: focus, measurements, observers, or interaction setup. ## Registered cleanup `createDirective` is the underlying cleanup mechanism. Its handler receives an element during hydration and may return a teardown function. Built-in directives such as `on` use the same mechanism, so event listeners are removed automatically. ```ts title="resize.ts" import { createDirective, html } from '@grainular/nord'; const observeSize = createDirective((node) => { const observer = new ResizeObserver(() => console.log(node.clientWidth)); observer.observe(node); return () => observer.disconnect(); }); export const Panel = () => html`
Content
`; ``` The same pattern applies to subscriptions, timers, browser observers, and manually attached event listeners. Return their teardown from the directive that created them. ## External DOM removal Cleanup is based on DOM connection, not on which code removed a node. If another library, browser API, or direct `element.remove()` call disconnects a Nørd-managed node, its registered cleanup still runs. This makes embedding Nørd in an existing page less fragile: Nørd does not require every removal to pass through a framework-owned renderer. ## Nested applications Any element can host a Nørd application, including an element rendered by another Nørd application. Each mount renders into its own target and updates its own DOM bindings. ```ts title="editor.ts" const EditorHost = () => html`
`; mount(App, { to: document.querySelector('#app') }); mount(Editor, { to: document.querySelector('#editor') }); ``` Nested applications do not need a provider, root context, or special integration layer. Use a shared grain or another ordinary JavaScript value when they should communicate. --- # TypeScript Nørd uses ordinary TypeScript types without requiring a framework-specific compiler. Most templates and components infer their types naturally; name a Nørd type only when it makes a public API clearer. ## Component types Any function returning `html` is a component. Let TypeScript infer that shape for local components, or use `PureComponent` when exporting a component whose signature should be explicit. ```ts title="greeting.ts" import { html, type PureComponent } from '@grainular/nord'; type GreetingProps = { name: string }; export const Greeting: PureComponent = ({ name }) => html`

Hej, ${name}!

`; ``` `PureComponent` is a convention, not a special runtime type. It accepts no argument for a component without props and one typed props object otherwise. ## Props and children Props are ordinary object types. `PropsWithChildren` adds a `children` field when a component accepts rendered content. ```ts title="card.ts" import { html, type PropsWithChildren } from '@grainular/nord'; type CardProps = PropsWithChildren<{ title: string }>; export const Card = ({ title, children }: CardProps) => html`

${title}

${children}
`; ``` Children can be a string, `ComponentFragment`, or `null`. They remain an explicit prop, so a component can instead expose named regions or several fragment props whenever that describes its API better. ## Component fragments `html` returns a `ComponentFragment`. You rarely need to write the type inside a component, but it is useful for functions that accept or return renderable content. ```ts title="content.ts" import { html, type ComponentFragment } from '@grainular/nord'; const withFrame = (content: ComponentFragment): ComponentFragment => html`
${content}
`; ``` A fragment is not a virtual DOM node or component instance. It is the value Nørd renders and hydrates, and it can be passed through ordinary TypeScript APIs. ## Directive types `createDirective` is generic over the host element. Name the element type when the directive relies on element-specific APIs; otherwise inference from `Element` is sufficient. ```ts title="select-on-focus.ts" import { createDirective, html } from '@grainular/nord'; const selectOnFocus = createDirective((input) => { const select = () => input.select(); input.addEventListener('focus', select); return () => input.removeEventListener('focus', select); }); const Search = () => html``; ``` The lower-level `Fragment` type exists for helpers that create directives or structs. Application components generally return `ComponentFragment` instead. ## Refs `createRef()` gives `current` the element type you expect. It is `null` until Nørd hydrates the host element, so normal null checking remains useful. ```ts title="search.ts" import { createRef, html, on, ref } from '@grainular/nord'; const searchInput = createRef(); const SearchButton = () => html` `; ``` Use a directive when behavior belongs to an element itself. Use a ref when another part of the component needs direct, typed access to that element. ## Using Nørd without TypeScript TypeScript is optional. The runtime API is plain JavaScript: templates are tagged literals, components are functions, and grains are callable values with methods. ```js title="counter.js" import { grain } from '@grainular/grains'; import { html, on } from '@grainular/nord'; export const Counter = () => { const count = grain(0); return html``; }; ``` Add types where they help communicate a boundary. Nørd does not require annotations, decorators, generated declarations, or a special template transform to work correctly. --- # Directives Directives receive an element and attach behavior, state, or lifecycle resources to it. They are small fragments placed directly inside an element’s opening tag. ## Where directives are used Interpolate a directive where an HTML attribute would normally appear. The directive itself renders no markup; it receives the host element while Nørd hydrates the template. ```ts title="counter.ts" import { html, on } from '@grainular/nord'; const increment = () => console.log('increment'); const Counter = () => html` `; ``` This keeps DOM behavior next to the element it affects. Directives target one element; use a [struct](/structs) when behavior owns a dynamic region of DOM instead. ## Built-in directives Nørd provides a small set of direct DOM primitives: - `on` attaches a typed event listener. - `attr` applies several static or reactive attributes from a record. - `mounted` runs after an element is connected to the document. - `ref` assigns the element to a ref object. - `portal` moves an element to another DOM target. Each remains ordinary JavaScript. There is no directive registry, naming convention, or template compiler involved. ## Reactive directive values Directives can use subscribables just like components do. `attr` accepts a record whose values may be reactive; it updates only the affected attributes. ```ts title="button.ts" import { attr, html, type Subscribable } from '@grainular/nord'; const SaveButton = ({ saving }: { saving: Subscribable }) => html` `; ``` Custom directives may subscribe to a value directly when they need to synchronize another DOM API. Keep that subscription within the directive so its cleanup follows the host element. ## Cleanup When a directive returns a function, Nørd registers it as cleanup for its host element. Built-in `on` uses this automatically, and custom directives use the same rule for observers, timers, subscriptions, or manually attached listeners. Cleanup follows DOM connection rather than a framework-owned unmount path. Removing the element through ordinary browser APIs or another library still triggers its registered teardown. ## Creating a directive `createDirective` turns a function receiving an element into a reusable directive. It is the extension point behind the built-ins and is covered in [Creating directives](/directives/custom). Start with built-ins whenever they express the behavior directly. Create a directive when the same DOM behavior needs a meaningful name or appears in more than one place. --- # Events and attributes Nørd provides small directives for common element behavior. `on` owns browser event listeners; `attr` applies one or more attributes when direct template interpolation is not the clearest fit. ## Event listeners with `on` `on` accepts a browser event name, listener, and optional listener options. TypeScript infers the event object from its name. ```ts title="counter.ts" import { html, on } from '@grainular/nord'; const increment = (event: MouseEvent) => { console.log(event.clientX); }; const Counter = () => html``; ``` The handler is an ordinary function. It can read grains, update state, call browser APIs, or delegate to another module. ## Event listener cleanup Nørd removes an `on` listener when its host element disconnects. You do not need an effect wrapper or separate teardown for normal element listeners. ```ts title="form.ts" const Form = () => html`
event.preventDefault())}>
`; ``` The same cleanup rule applies when another library or direct DOM code removes the element. ## Attributes with `attr` Use `attr` when attributes are assembled as a record, such as a reusable component helper or a dynamic set of `data-*` attributes. ```ts title="button.ts" import { attr, html } from '@grainular/nord'; const buttonAttributes = { type: 'button', 'data-variant': 'primary', }; const Button = () => html``; ``` For a single known attribute, direct interpolation remains the simplest form: `aria-label="${label}"`. ## Reactive attributes Record values may be subscribables. `attr` reads their initial value, subscribes to changes, and releases every subscription with the host element. ```ts title="save-button.ts" import { attr, html, type Subscribable } from '@grainular/nord'; const SaveButton = ({ saving }: { saving: Subscribable }) => html` `; ``` HTML boolean attributes use presence semantics: a truthy `disabled` produces `disabled`, while a falsy value removes it. ARIA attributes are ordinary string attributes, so `aria-busy` becomes `"true"` or `"false"`. ## Native DOM properties `attr` deliberately writes attributes. Many HTML attributes reflect to a DOM property, but some browser APIs are property-only or have different semantics. Use a custom directive or a ref when you need the property itself. ```ts title="input-value.ts" import { createDirective, html } from '@grainular/nord'; const value = createDirective((input) => { input.value = 'Initial value'; }); const Input = () => html``; ``` This distinction keeps templates honest: attributes describe markup, while directives can use the full DOM API when behavior requires it. --- # Refs and lifecycle Refs expose DOM elements, while lifecycle directives manage resources attached to them. Both work with ordinary browser nodes rather than component instances. ## Creating a ref `createRef` creates an object with a `current` field. Pass the expected element type to retain native DOM autocomplete and checking. ```ts title="search.ts" import { createRef } from '@grainular/nord'; const searchInput = createRef(); ``` Before hydration, `current` is `null`. A ref does not query the document or create an element by itself. ## Applying a ref Apply `ref` to the element that should populate the reference. ```ts title="search.ts" import { html, ref } from '@grainular/nord'; const Search = () => html` `; ``` Once Nørd hydrates the input, `searchInput.current` is that exact `HTMLInputElement`. ## Mounted callbacks Use `mounted` when behavior belongs to the host element itself and must wait until it is connected to the document. ```ts title="focus.ts" import { html, mounted } from '@grainular/nord'; const focus = mounted((node) => { (node as HTMLInputElement).focus(); return () => {}; }); const Search = () => html``; ``` This is suitable for focus, element measurement, and browser observers. A mounted callback receives the element directly, so it often removes the need for a ref. ## Cleanup callbacks Return a function from `mounted` or `createDirective` to release anything the element owns. ```ts title="observe.ts" const observe = mounted((node) => { const observer = new ResizeObserver(() => console.log(node.getBoundingClientRect().width)); observer.observe(node); return () => observer.disconnect(); }); ``` Nørd runs that cleanup when the host disconnects, including when it is removed through ordinary DOM APIs outside Nørd. ## Choosing between refs and directives Use a ref when another event handler or part of the component needs to access an element. Use `mounted` or `createDirective` when behavior is attached to one element and can stay colocated with it. Refs are an escape hatch for direct access, not a replacement for ordinary declarative template composition. --- # Portals Portals move an element into a target outside its original template position. The element keeps its Nørd bindings, directives, and normal DOM lifetime. ## Creating a portal Pass a target element to `portal` and place the directive on the element that should move. ```ts title="dialog.ts" import { html, portal } from '@grainular/nord'; const portalTarget = typeof document === 'undefined' ? undefined : document.body; const Dialog = () => html`

Dialog

`; ``` The section is declared alongside its component but becomes a child of `document.body` after browser hydration. Portals are a browser concern, so guard the target when the component may also render on the server. ## Choosing a target Use a stable element that exists before the portal hydrates. `document.body` is a good default for dialogs, menus, and notifications; a dedicated overlay root is useful when an application needs to control stacking or styling. ```ts title="overlay-root.ts" const overlayRoot = document.querySelector('#overlays'); const Menu = () => html`
Menu
`; ``` `portal` throws when the target is missing, which makes an incorrectly ordered setup fail close to its source. ## Reactive portal content The portal only changes where the host element lives. Interpolations, grains, directives, and structs inside it continue to update normally. ```ts title="toast.ts" import { html, portal, type Subscribable } from '@grainular/nord'; const Toast = ({ message }: { message: Subscribable }) => html` `; ``` There is no separate portal renderer or state boundary to coordinate. ## Cleanup When Nørd disposes the portalled element, the portal directive removes it from its target. Cleanup returned by directives inside the element still follows that element’s DOM lifetime. Treat the target as portal infrastructure rather than component content. The component owns the moved element; the target only provides its destination. ## Common use cases Portals are useful for UI that must escape local clipping, stacking, or layout constraints: - dialogs and popovers; - context menus and tooltips; - toast and notification regions; - drag previews. Use a portal for DOM placement, not for communication. Pass grains, callbacks, or ordinary props when portalled content needs to coordinate with its source component. --- # Creating directives Custom directives package direct DOM behavior into composable functions. They are the smallest extension point in Nørd: receive an element, use normal browser APIs, and return cleanup when needed. ## `createDirective` `createDirective` turns an element handler into a fragment that can be interpolated in an opening tag. ```ts title="focus.ts" import { createDirective, html } from '@grainular/nord'; const autofocus = createDirective((input) => { input.focus(); }); const Search = () => html``; ``` The handler runs while Nørd hydrates the element. Use `mounted` instead when behavior must wait until the element is connected to the document. ## Rendering static attributes Pass an optional second callback when a directive also needs to contribute attributes during server rendering. Its return value is written into the host element's opening tag; the browser handler still runs normally when the page is mounted. ```ts title="current-page.ts" const currentPage = createDirective( (link) => { link.setAttribute('aria-current', 'page'); }, () => 'aria-current="page"', ); const Navigation = () => html`Documentation`; ``` Return complete, valid attribute markup. The snapshot is raw HTML output, so escape any dynamic attribute values yourself; for ordinary known attributes, direct template interpolation is usually clearer. ## Accepting values Wrap `createDirective` in a function to create a directive with configuration. ```ts title="class-name.ts" const className = (value: string) => createDirective((node) => { node.classList.add(value); }); const Notice = () => html`

Saved

`; ``` The outer function is ordinary JavaScript. Use parameters, closures, generics, and module imports as needed. ## Returning cleanup Return a function whenever the directive creates a resource that outlives the current call stack. ```ts title="resize.ts" const observeSize = createDirective((node) => { const observer = new ResizeObserver(() => console.log(node.getBoundingClientRect().width)); observer.observe(node); return () => observer.disconnect(); }); ``` Nørd calls the teardown when the host disconnects, including removal performed outside Nørd. ## Working with reactive values Directives can subscribe to a value directly. Set the initial DOM state first, then return the subscription cleanup. ```ts title="text.ts" import { createDirective, html, type Subscribable } from '@grainular/nord'; const text = (value: Subscribable) => createDirective((node) => { node.textContent = value(); return value.subscribe((next) => { node.textContent = next; }); }); const Status = ({ message }: { message: Subscribable }) => html`

`; ``` For ordinary text and attributes, interpolate the subscribable directly instead. A custom directive is useful when the target is a browser API rather than template content. ## Composition Directives compose by appearing together on the same element. Each owns its own setup and cleanup. ```ts title="search.ts" const Search = () => html` `; ``` Keep directives narrow and name them for the behavior they add. A directive should not need a component wrapper unless it also owns markup or state composition. --- # Control flow Nørd structs control regions of the DOM: a conditional branch, a list of items, or the result of an asynchronous operation. They are fragments, so they can appear anywhere a template interpolation can appear. ## How structs work Each struct has a small comment anchor in the rendered template. When its input changes, it adds, removes, or replaces only the nodes immediately before that anchor. The rest of the component remains untouched. This keeps control flow local. A condition does not rerun its parent component; a list update does not recreate its neighbours. ## Built-in structs Use `$if` and `$switch` for conditional content. `$each` renders collections with keyed reconciliation. `$await`, `$suspend`, and `$try` handle asynchronous or fallible work. `$render`, `$tag`, and `$unsafeHtml` are lower-level tools for the cases where the normal template syntax is not the right fit. ## Reactive dependencies Most control-flow structs accept either a subscribable value or a getter. A subscribable keeps its region updated; a getter is evaluated when the struct mounts and then left alone. ```ts import { grain } from '@grainular/grains'; import { $if, html } from '@grainular/nord'; const visible = grain(true); const Message = () => html`${$if(visible).$then(() => html`

Visible

`)}`; ``` ## DOM ownership A struct owns the nodes it renders. Replacing a branch, removing an item, or unmounting the containing component disconnects those nodes and runs their registered cleanup. Nodes outside the struct remain in place. This is why struct callbacks return fragments instead of already-mounted nodes: Nørd can give each rendered region a complete lifecycle. ## Creating a struct The built-ins cover ordinary application flow. Reach for `createStruct` only when you need a reusable primitive that manages its own DOM region, such as an integration with a browser API or a specialised renderer. See [Creating structs](/structs/custom) for the lower-level API. --- # Conditional rendering Conditional structs update only the active branch when their condition changes. ## `$if` Pass `$if` a boolean subscribable and provide the fragment to render when it is true. Only the branch is replaced when the value changes. ```ts import { grain } from '@grainular/grains'; import { $if, html } from '@grainular/nord'; const signedIn = grain(false); const Account = () => html` ${$if(signedIn).$then(() => html``)} `; ``` ## Else branches Chain `.$else` when both states need content. The callbacks are evaluated only for the branch that becomes active. ```ts const Status = () => html` ${$if(signedIn) .$then(() => html`

Welcome back.

`) .$else(() => html`Sign in`)} `; ``` ## `$switch` `$switch` selects one branch from a subscribable or getter. It is useful when several mutually exclusive states would make nested `$if` calls harder to read. ```ts import { grain } from '@grainular/grains'; import { $switch, html } from '@grainular/nord'; const state = grain<'idle' | 'loading' | 'ready'>('idle'); const Panel = () => html` ${$switch(state) .$case('idle', () => html``) .$case('loading', () => html`

Loading…

`) .$default(() => html`

Ready.

`)} `; ``` ## Matching cases Cases use strict equality. Keep the values simple and stable, and always provide `.$default` for a state that is not represented by a case. ## Preserving surrounding DOM The surrounding template is not part of the conditional region. Inputs retain their value and neighbouring components retain their state while a branch changes. ```ts const Page = () => html` ${$if(signedIn).$then(() => html`

Private content

`)} `; ``` --- # Lists The `$each` struct maps iterable values to independently managed DOM fragments. ## Rendering an iterable Use `$each` with a list and a render callback. Prefer `.$withKey` whenever items have a stable identifier. ```ts import { grain } from '@grainular/grains'; import { $each, html } from '@grainular/nord'; const tasks = grain([ { id: 'write', title: 'Write the guide' }, { id: 'publish', title: 'Publish it' }, ]); const TaskList = () => html`
    ${$each(tasks) .$withKey((task) => task.id) .$as((task) => html`
  • ${task.title}
  • `)}
`; ``` ## Reactive collections When the source is subscribable, `$each` reconciles it whenever it changes. Set a new array when updating a grain so subscribers receive the change. ```ts tasks.update((current) => [...current, { id: 'review', title: 'Review the guide' }]); ``` Existing keyed items keep their DOM and lifecycle while Nørd inserts, removes, or moves only what changed. ## Item identity Keys describe an item's identity, not its position. They must be unique among the current items and remain the same when an item moves. `.$as` without `.$withKey` uses the item value itself as the key. That is fine for unique, stable object references, but a key function is safer for primitives, duplicate values, and objects recreated during updates. The second render argument is a subscribable index. Use it directly when an item's displayed position should update after reordering. ```ts $each(tasks) .$withKey((task) => task.id) .$as((task, index) => html`
  • ${index}. ${task.title}
  • `); ``` ## Adding and removing items Adding an item renders one new fragment. Removing an item disconnects its nodes and runs cleanup registered by its directives and child components. Reordering keyed items moves the existing nodes instead of recreating them. ## Empty collections `$each` renders nothing for an empty array by default. Chain `.$empty` after `.$as` when the list owns its empty-state markup. The fallback is removed when items are added and restored when they are all removed. ```ts const Tasks = () => html`
      ${$each(tasks) .$withKey((task) => task.id) .$as((task) => html`
    • ${task.title}
    • `) .$empty(() => html`
    • No tasks yet.
    • `)}
    `; ``` --- # Async rendering Nørd structs can connect promises and suspended work to a local DOM region. ## `$await` Pass `$await` a promise and define the fragment for its fulfilled value. The promise affects only this region of the page. ```ts import { $await, html } from '@grainular/nord'; const user = fetch('/api/user').then((response) => response.json() as Promise<{ name: string }>); const Profile = () => html` ${$await(user).$then((value) => html`

    ${value.name}

    `)} `; ``` ## Pending content Use `.$pending` to render while the promise is unresolved. During server rendering, Aurora and Nørd render this pending branch because the server does not wait for browser work. ```ts $await(user) .$then((value) => html`

    ${value.name}

    `) .$pending(() => html`

    Loading profile…

    `); ``` ## Resolved content `.$then` receives the resolved value. It returns an ordinary fragment, so resolved content can contain components, directives, and further control flow. ## Error content Add `.$catch` to display a local failure state. The callback receives the rejection value as `unknown`. ```ts $await(user) .$then((value) => html`

    ${value.name}

    `) .$pending(() => html`

    Loading profile…

    `) .$catch(() => html`

    Could not load the profile.

    `); ``` ## `$suspend` `$suspend` is a compact form for components that may return a fragment or a promise of one. It is especially useful for lazy imports. ```ts import { $suspend, html } from '@grainular/nord'; const Settings = () => html`${$suspend(() => import('./settings').then(({ SettingsPanel }) => SettingsPanel()), { pending: () => html`

    Loading settings…

    `, error: () => html`

    Settings are unavailable.

    `, })}`; ``` ## `$try` `$try` is a synchronous error boundary for rendering code that might throw. It catches errors raised while its initial fragment is created; it does not catch event-handler errors or later promise rejections. ```ts import { $try, html } from '@grainular/nord'; const SafePreview = () => html` ${$try(() => renderPreview()).$catch(() => html`

    Preview unavailable.

    `)} `; ``` --- # Other built-in structs Nørd includes focused structs for dynamic rendering and direct HTML integration. ## `$render` `$render` displays the fragment held by a subscribable and replaces that region whenever the fragment changes. It is useful when the view itself is reactive. :::Warning{title="The region is replaced, not diffed"} `$render` disconnects the previous fragment and mounts the next one in its place. It does not compare the two fragments or preserve their DOM nodes. Keep the rendered region small and avoid using it for rapidly changing values; bind those values directly or use a more focused control-flow struct instead. ::: ```ts import { grain } from '@grainular/grains'; import { $render, html } from '@grainular/nord'; const view = grain(html`

    Loading…

    `); const Screen = () => html`
    ${$render(view)}
    `; view.set(html`

    Ready.

    `); ``` ## `$tag` `$tag` creates an element programmatically. Use it when the tag name is dynamic or when a small integration needs direct access to the created element. Normal HTML templates remain clearer for ordinary markup. ```ts import { attr, $tag, html } from '@grainular/nord'; const Action = () => html`${$tag({ as: 'button', children: 'Save', use: [attr({ type: 'button' })], })}`; ``` ## `$unsafeHtml` `$unsafeHtml` inserts a raw HTML string. It does not sanitize its input, so only use it for markup your application fully controls. ```ts import { $unsafeHtml, html } from '@grainular/nord'; const Article = () => html`${$unsafeHtml('

    Trusted HTML.

    ')}`; ``` :::Caution{title="Never render untrusted HTML"} `$unsafeHtml` does not sanitize its input. Passing user-authored or otherwise untrusted text to it creates an XSS vulnerability. Sanitize HTML before it reaches this API, or render the source through a safe renderer instead. ::: ## Choosing the right primitive Use template interpolation for normal content, `$if` and `$each` for control flow, and `$await` for promises. `$render` fits a reactive fragment, `$tag` fits programmatic element creation, and `$unsafeHtml` is the deliberate escape hatch for trusted raw markup. --- # Creating structs Custom structs expose specialized rendering behavior without extending Nørd itself. ## `createStruct` `createStruct` creates a fragment around a comment anchor. Its callback receives that anchor after the template mounts and may insert nodes immediately before it. ```ts import { createStruct, html } from '@grainular/nord'; const timestamp = createStruct((anchor) => { const time = document.createElement('time'); time.textContent = new Date().toLocaleTimeString(); anchor.before(time); }); const Clock = () => html`Rendered at ${timestamp}`; ``` ## Rendering static output Pass a second callback when the struct needs server-rendered output. It returns the HTML string included in the static snapshot; the first callback still supplies browser behaviour after hydration. ```ts const greeting = createStruct( (anchor) => { const message = document.createElement('p'); message.textContent = 'Hello from the browser.'; anchor.before(message); }, () => '

    Hello from the server.

    ', ); ``` ## Mounting dynamic behavior The callback runs once per mounted struct. Use it to connect an API that cannot be expressed by a directive or one of the built-in structs. Keep the setup focused: create or hydrate the nodes the struct owns, then place them relative to the anchor. ## Tracking DOM nodes Keep references to every node your struct creates if it may replace or remove them later. When a struct controls several nodes, insert them together with `anchor.before(...nodes)` and remove the same nodes during an update or cleanup. Built-in structs already handle keyed lists, promises, and conditional branches. A custom struct should add a capability, not recreate those primitives. ## Cleanup Return a cleanup function for timers, observers, subscriptions, or nodes that need explicit disposal. Nørd runs it when the anchor leaves the document, including when an ancestor is removed outside Nørd. ```ts const tickingTime = createStruct((anchor) => { const time = document.createElement('time'); const update = () => (time.textContent = new Date().toLocaleTimeString()); update(); const interval = window.setInterval(update, 1_000); anchor.before(time); return () => window.clearInterval(interval); }); ``` --- # Server rendering Nørd can turn the same component fragments used in the browser into HTML on the server. It does not prescribe a server, router, or deployment model; rendering HTML is a small function call you can use in a static build, an HTTP handler, or a larger application. ## `renderToString` `renderToString` evaluates a component and returns its current HTML snapshot. ```ts title="page.ts" import { html, renderToString } from '@grainular/nord'; const Page = () => html`

    Hello from Nørd

    `; const page = renderToString(Page); //

    Hello from Nørd

    ``` Nørd does not add a document shell. Put the result into the HTML document your server or build process already creates. ```ts title="document.ts" const document = ` Example ${renderToString(Page)} `; ``` :::Caution `renderToString` escapes interpolated values during serialization, but cannot guarantee that malicious JavaScript is stripped. If you include user generated values in serialization, make sure to sanitize them accordingly. ::: ## Static generation Static generation is the natural use of server rendering: evaluate each route during a build, then write the resulting document to the output directory. The browser receives ordinary HTML that works before JavaScript loads. Aurora uses this model for documentation pages, but Nørd itself has no dependency on Aurora. Any build tool, server runtime, or file writer can call `renderToString`. ## Client activation `mount` activates a browser application by creating a fresh DOM fragment, connecting its reactive bindings, then replacing the target's existing children in one operation. ```ts title="client.ts" import { mount } from '@grainular/nord'; import { Counter } from './counter'; mount(Counter, { to: document.querySelector('#counter') }); ``` This is deliberately destructive hydration. Nørd does not walk server-rendered DOM to recover component state; it mounts a live application quickly and swaps its target region once it is ready. ## Island architecture Render the static page normally, then reserve small, independent hosts for the parts that require a browser. Each island can load and mount independently. ```html title="page.html"

    Mostly static content

    ``` ```ts title="islands.ts" import { mount } from '@grainular/nord'; import { Counter } from './counter'; import { NewsletterSignup } from './newsletter-signup'; mount(NewsletterSignup, { to: document.querySelector('#newsletter-signup') }); mount(Counter, { to: document.querySelector('#counter') }); ``` Islands do not need a shared application root. They can communicate through ordinary shared modules, browser APIs, or the state layer you already use. An island may also live inside another Nørd application when that composition is useful. ## State and serialization Component state lives in JavaScript closures. It is not serialised into the HTML snapshot and cannot be reconstructed by reading it back from the DOM. A client mount therefore starts with its own state. For static pages and self-contained islands, this is usually the simplest model. When the browser needs data from the server, provide it explicitly: embed a small JSON payload, fetch an endpoint, or use the page URL and browser storage as appropriate for the application. ## When not to hydrate Do not mount an application merely because a page was rendered on the server. Static content, navigation links, articles, and ordinary forms already work as HTML. Mount only the controls that need reactive state or browser APIs. Nørd is particularly effective when server rendering provides the page and small islands add interaction. It is not designed for incremental DOM hydration or for recovering a large application tree from a server snapshot. --- # Tooling Nørd does not require a compiler plugin, a custom browser runtime, or proprietary developer tools. Its templates are ordinary tagged template literals and components are ordinary TypeScript functions, so established editor, debugger, and browser tooling remains useful. ## Editors ### VS Code Install [Nørd for VS Code](https://marketplace.visualstudio.com/items?itemName=iamsebastiandev.grainular-nord-vscode) for HTML highlighting, element completion, HTML comments, and folding in Nørd templates. ### Zed [Zed](https://zed.dev/) supports tagged templates in its built-in language tooling, so it works well with Nørd templates without an additional extension. ## Formatting Both [Prettier](https://prettier.io/) and [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) format Nørd templates. Oxfmt now formats the embedded HTML in tagged templates as well as the surrounding TypeScript, so it is a supported formatter for Nørd projects. ## Debugging Nørd does not need a dedicated browser-devtools extension. Use your browser's regular developer tools to inspect the rendered DOM, styles, events, network activity, and performance. Nørd renders standard DOM nodes, so the Elements panel shows the same nodes the browser is operating on. Use the editor or browser debugger for breakpoints in components, directives, event handlers, and callbacks. A regular `debugger` statement works too: ```ts title="counter.ts" import { html, on } from '@grainular/nord'; export const Counter = () => { const increment = () => { debugger; // Inspect local values and step through ordinary TypeScript here. }; return html``; }; ``` When a problem involves state updates, start by placing a breakpoint where the value changes or where its callback runs. When it involves rendering, inspect the resulting DOM in the Elements panel. This is usually more direct than switching to framework-specific tools. --- # API reference This reference lists the public exports provided by Nørd and Grains. ## Application ### `mount` `mount(component, { to })` creates and hydrates a component fragment inside an `Element` or `DocumentFragment`, replacing the target’s existing children. It returns a cleanup function for the mounted application and throws when no valid target is supplied. ### `renderToString` `renderToString(component)` evaluates a component and returns its HTML snapshot as a string. It is the server-side entry point for static output; browser activation remains a separate concern and can be applied only where it is needed. ### `syncReactive` `syncReactive({ get, subscribe })` adapts an external reactive source into a readonly Nørd subscribable. The source is read synchronously through `get`, subscribed lazily when Nørd needs it, and unsubscribed once its final consumer disconnects. ### `Subscribable` `Subscribable` is Nørd's reactive input contract: a callable getter paired with `subscribe(listener)`. Nørd accepts a compatible value wherever a reactive interpolation, attribute, or control-flow source is expected; Grains are one implementation of this contract. ```ts type Subscribable = { (): T; subscribe(listener: (value: T) => void): () => void; }; ``` ## Templates and components ### `html` `html` is the tagged template function used to create a `ComponentFragment`. Static strings become HTML, while interpolated primitives, fragments, and subscribable values become the corresponding dynamic parts of that fragment. ### `ComponentFragment` `ComponentFragment` is the value returned by `html` and by Nørd components. It represents a fragment that can be rendered to an HTML string or hydrated into live DOM nodes, allowing fragments to compose without a separate virtual-node type. ### `Fragment` `Fragment` is the lower-level render unit behind component fragments, directives, structs, primitive values, and reactive bindings. Most application code should use `ComponentFragment`; use `Fragment` when writing a reusable low-level primitive such as a directive or struct. ### `PureComponent` `PureComponent` is the conventional type for a component function returning a `ComponentFragment`. It accepts no argument when a component has no props, or a typed props object when it does; any function with the same shape is a valid Nørd component. ### `ComponentProps` `ComponentProps` is the base constraint for component prop objects. It is useful when writing reusable component helpers or generic component types, while most components can simply infer their own explicit props object. ### `PropsWithChildren` `PropsWithChildren` adds a `children` field to a props object. Children are passed explicitly as a string, `ComponentFragment`, or `null`; they are ordinary props rather than a separate slot or special component syntax. ## Directives ### `attr` `attr(record)` applies each key/value pair in a record as an attribute on the host element. Values may be static or subscribable; reactive attributes stay synchronized and their subscriptions are cleaned up with the element. ### `on` `on(event, listener, options?)` attaches a typed browser event listener to the host element. The listener is removed automatically when that element is disconnected, so event behavior can remain colocated with the template that owns it. ### `mounted` `mounted(callback)` runs a callback when its host element enters the document. A cleanup function returned by the callback is registered for removal, making it suitable for browser integrations that need a real, connected element. ### `portal` `portal(target)` moves its host element into another DOM element while preserving the directive’s normal lifecycle. It is useful for overlays, dialogs, and similar UI that must render outside the local DOM hierarchy. ### `ref` and `createRef` `createRef()` creates a `{ current: null }` reference object, and `ref(reference)` assigns the hydrated host element to it. Refs provide direct, typed access to an element when a directive or event callback is not the right place for that work. ### `createDirective` `createDirective(handler)` creates a directive from a function receiving the host element. The handler may return a cleanup function, which Nørd invokes when the element is removed, and forms the extension point behind custom element behavior. ## Structs ### `$if` `$if(condition)` creates a conditional region from a boolean getter or subscribable. Chain `.$then(...)` for the true branch and optionally `.$else(...)` for the false branch; the active fragment is replaced when a reactive condition changes. ### `$switch` `$switch(value)` selects one fragment from several cases. Chain `.$case(value, render)` for matching values and finish with `.$default(render)` for the fallback; a subscribable source updates the selected fragment as its value changes. ### `$each` `$each(source)` renders an array from a getter or subscribable and reconciles it when the source changes. Use `.$as(render)` for stable object identities or `.$withKey(key).$as(render)` to give entries explicit, durable DOM identity. ### `$await` `$await(value)` renders a promise or immediate value through `.$then(render)`, with optional `.$pending(render)` and `.$catch(render)` states. It provides a small async region without requiring the surrounding component to become asynchronous. ### `$suspend` `$suspend(component, { pending, error })` is an async component boundary with required loading and error fragments. It accepts a function that returns a component fragment or a promise for one, which makes it particularly useful for dynamically imported UI. ### `$try` `$try(render).$catch(fallback)` creates a synchronous error boundary for a dynamic region. It catches errors thrown while the initial fragment is created and renders the fallback instead; it does not handle errors that occur later in event handlers or asynchronous work. ### `$render` `$render(source)` renders the `ComponentFragment` held by a subscribable and replaces the region whenever that fragment changes. It is useful when the fragment itself, rather than a value inside it, is the reactive unit. ### `$tag` `$tag({ as, children, use }, onMount?)` creates an HTML element programmatically, with optional child content, directives, and lifecycle callback. It is mainly useful when the tag name is dynamic or when element creation needs to be driven by data rather than static template markup. ### `$unsafeHtml` `$unsafeHtml(trustedHtml)` inserts a raw HTML string without parsing it as a Nørd template. It does not sanitize its input, so it must only receive HTML that is fully trusted and never unsanitized user content. ### `createStruct` `createStruct(handler, snapshot?)` creates a dynamic region anchored by a comment node. The handler owns DOM inserted around that anchor and may return cleanup; the optional snapshot supplies its server-rendered HTML, making this the extension point for custom structs. ## Grains ### `grain` `grain(initialValue, compare?)` creates a synchronous writable reactive value. Call it to read its current value, use `.set(next)` or `.update(updater)` to change it, and `.subscribe(listener)` to observe changes; updates notify subscribers only when the comparison reports a difference. ### `derived` `derived(source, map)` creates a readonly grain by transforming one source grain. Reads apply the mapping immediately, and subscriptions are forwarded from the source with the mapped value. ### `combined` `combined(sources)` combines several grains into one readonly grain containing their current values as a typed tuple or array. It notifies subscribers when any source changes, which is useful when a calculation depends on several independent values. ### `flattened` `flattened(nested)` turns a grain containing another grain into one readonly grain. It follows the current inner grain, switches subscriptions when the outer grain changes, and exposes only the inner value. ### `readonly` `readonly(source)` exposes the read and subscribe surface of a grain without its write methods. It is a lightweight way to share state with consumers while keeping mutation with the owner. --- # Large Language Model Documentation Nørd production builds provide optimized documentation structures for Large Language Models (LLMs) and AI agents. We adhere to the standard [llms.txt](https://llmstxt.org/) convention so AI assistants can ingest the documentation without parsing HTML. The following files are intended to be available from the documentation root: - [`/llms.txt`](/llms.txt): A concise summary of the framework, key concepts, and high-level API reference. - [`/llms-full.txt`](/llms-full.txt): The complete documentation, including detailed guides, API signatures, and examples.