Skip to main content

Custom Components

StageRoutine components are reactive elements that wrap HTML or SVG DOM nodes.

Component Lifecycle​

Elements automatically participate in StageRoutine's lifecycle (onMount, onActivate, onDeactivate, onUnmount).

To learn how to manage active resources like timers, video playback, and GPU performance, see Element Lifecycle.

Writing a Component Function​

Wrap a DOM node with Element():

import { Element, type DOMElement, type ElementOptions } from "stageroutine";

export interface BadgeOptions extends ElementOptions {
label: string;
}

export function Badge(options: BadgeOptions): DOMElement {
const node = document.createElement("div");
node.className = "my-badge";
node.innerText = options.label;

return Element(node, {
origin: "center",
...options,
});
}

Animate custom drawing​

Give Element() numeric props and an onDraw callback. Set every initial value explicitly. Custom values have no defaults; you choose their units.

const meter = Element(document.createElement("div"), {
props: { progress: 0 }, // Fraction from 0 to 1.
onDraw({ progress }, node) {
node.textContent = `${Math.round(progress * 100)}%`;
},
});

stage.scene("Progress").with(meter);
stage.pause();
meter.to({ progress: 1 }).duration(1); // Seconds.
stage.pause();

onDraw receives a read-only copy of the custom values at creation, on stage updates, and when restoring a snapshot. Each animation frame resolves all properties before drawing. Going backward restores both the values and the drawing. The stage stops its animation loop when transitions finish.

Use onDraw to update SVG attributes, draw a canvas, or update a third-party visualization. Build the chart once, then update its geometry from these values. Keep timers and animation loops out of the callback.

Property names must differ from built-in element properties. Values must be finite numbers. Use transitions to control their timing.

Keep the drawing callback inside your component function. Expose initial values as component options:

import { Element, type ElementOptions, transform } from "stageroutine";

interface MeterOptions extends ElementOptions {
progress?: number; // Fraction from 0 to 1. Default: 0.
}

export function Meter({ progress = 0, ...options }: MeterOptions = {}) {
const node = document.createElement("div");
return Element(node, {
...options,
props: { progress },
onDraw({ progress }, node) {
node.textContent = `${Math.round(progress * 100)}%`;
},
});
}

const turn = transform();
const meter = Meter({ position: [80, 45], origin: "center" }).decorate(turn);
stage.scene("Progress").with(meter);
stage.pause();
meter.to({ progress: 1 }).duration(1); // Seconds.
turn.to({ rotationY: -20 }).duration(1); // Degrees over one second.
stage.pause();

The component maps progress to its drawing. The presentation sets timing and pauses. Standard element properties handle placement and opacity. Reusable transform effects handle tilt and perspective. Keep drawing deterministic: the same property values must produce the same content. Start transitions from the presentation; mount and activate hooks can run again during navigation.

Using JSX​

StageRoutine includes a lightweight JSX runtime for declaring markup without React overhead:

/** @jsxImportSource stageroutine */
import { Element } from "stageroutine";

export function Card({ title, content, ...options }) {
const vdom = (
<div class="card-container">
<h3>{title}</h3>
<p>{content}</p>
</div>
);

return Element(vdom, options);
}

Component styles​

The component function creates content and exposes options. A companion stylesheet defines its default appearance. See TypeScript or CSS? for choosing where a setting belongs.

Import the stylesheet from the component file:

// Badge.ts
import "./Badge.css";

Use theme variables in the stylesheet. The badge then follows both stage themes and its local theme option:

/* Badge.css */
@layer sr-components {
.my-badge {
background: var(--sr-surface);
border: var(--sr-surface-border);
border-radius: var(--sr-radius);
color: var(--sr-text);
padding: var(--sr-space-3xs) var(--sr-space-xs);
font-family: var(--sr-font-mono);
}
}

sr-components holds component defaults. A presentation's normal, unlayered CSS can override them.

Choose palette values at the call site instead of hardcoding them in the component:

const badge = Badge({
label: "Ready",
theme: { text: "#a6e3a1", surface: "#11111b" },
});

The component does not need to read the active theme in TypeScript. CSS inherits the tokens. Use element properties for stage position, dimensions, and animation state; use layout to position separate stage elements.

Extend ElementOptions​

When your options interface extends ElementOptions, your component automatically inherits:

  • style: Typed inline CSS overrides via csstype.
  • theme: Localized token overrides ({ surface: "...", primary: "..." }).
  • className: Custom class name merging.
  • onActivate / onDeactivate: Scene visibility callbacks.
  • onMount / onUnmount: DOM lifecycle callbacks.
export interface BadgeOptions extends ElementOptions {
label: string;
}