Components and JSX
Build inert JSX descriptions with HTML and SVG spellings, explicit listeners, and trusted raw HTML.
JSX creates a description
The automatic JSX helpers create inert descriptions. Creating one does not call its component, touch the DOM, or store render state on the description. The renderer calls a function component once for each instance and uses its result to build the page or island.
Descriptions expose a type, props, and key, and carry the runtime's description brand. Factories copy props rather than mutating the object passed by the caller. A description can be reused or copied as an ordinary object. Use the public h helper to construct one without JSX; isDescription checks the brand, and flattenChildren applies the same child normalization used by the runtime.
With h, variadic children replace props.children; when no variadic children are supplied, h uses props.children. The JSX factory's explicit key argument takes precedence over props.key, and a missing key is normalized to null.
Child values
Children may be strings, finite numbers, descriptions, scalar-valued reactive signals, or arrays of those values. Nested arrays are flattened. Null, undefined, and booleans are omitted, while an empty string is retained. Functions, promises, symbols, bigints, non-finite numbers, and arbitrary objects are errors.
A component may return one description, an array, a string or number, or an empty value. These are synchronous results. A promise nested in the rendered tree is an error; the runtime does not suspend or await nested components. zfb may await a top-level page function before rendering its result.
Fragments and arrays returned by components preserve sibling groups without adding a visible element. Plain intrinsic child arrays are just traversal lists. Copying a description does not create a new component instance by itself.
Table and select children
Table and select structure must follow the same nesting rules in static rendering, hydration, and fresh mounts. In static rendering, a table accepts caption, colgroup, thead, tbody, or tfoot as direct children; a tr directly under table fails with ZR_PARSER_CONTEXT. Include <tbody> explicitly, even though an HTML parser inserts one for raw <table><tr> markup, so the server output and DOM adopted during hydration have the same structure. Table sections such as tbody, thead, and tfoot require tr; tr requires td or th; select requires option or optgroup; and optgroup requires option. A component such as <Row />, a Fragment, or a For region cannot be a direct child in those positions. An option can contain only static scalar text. The static renderer reports invalid child positions with ZR_PARSER_CONTEXT.
Build structural children by calling a plain helper that returns intrinsic element descriptions. Calling rowElement(row) works when it returns a tr; rendering <Row /> as a tbody child does not. Use the same pattern for helpers returning option or optgroup elements directly under select. For reactive table rows, see Conditionals and lists.
function rowElement(row: { name: string }) {
return <tr><td>{row.name}</td></tr>;
}
function ResultsTable({ rows }: { rows: { name: string }[] }) {
return (
<table>
<tbody>{rows.map((row) => rowElement(row))}</tbody>
</table>
);
}Use HTML and SVG names
Intrinsic attributes follow HTML and SVG spelling. Use class, for, readonly, tabindex, and viewBox. React spellings such as className, htmlFor, charSet, autoComplete, readOnly, tabIndex, onClick, strokeWidth, and dangerouslySetInnerHTML fail validation; they are not rewritten or emitted as-is. When an HTML or SVG spelling is supported, the error names it (for example, use `charset` instead of `charSet` ); onClick suggests on:click. Unsupported standard attributes fail without a suggested replacement. Data and ARIA attributes accept scalar string, number, or boolean values; a boolean ARIA value is serialized as the corresponding string, including false.
If you use ESLint, see the v3 migration guide for a file-scoped override: eslint-plugin-react can reject or autofix these HTML spellings.
Boolean HTML attributes use presence for true and absence for false. Enumerated attributes such as contenteditable are strings. Null and undefined omit ordinary attributes. A reactive value is allowed for a supported scalar attribute, subject to the form-control rules in the forms guide, including its display-only exception for text inputs and textarea with static readonly={true} or disabled={true}.
The hidden attribute accepts booleans only, not until-found, and inert is boolean. The runtime does not add hiding CSS, so the page's authored styles remain responsible for display behavior.
An ol accepts a numeric or string start value and a boolean reversed attribute.
The finite HTML vocabulary includes standard head and metadata attributes such as meta[property], link[as], integrity, and external script[defer]. Write Open Graph tags, preload links, and deferred scripts as ordinary JSX; these do not need a rawHtml wrapper. Attribute values keep their HTML categories: true booleans use presence, enumerated values such as spellcheck accept strings or booleans, download is overloaded, and ol[start] is numeric. Unknown standard names remain errors, and custom elements keep their string-only attribute contract.
See the supported HTML and SVG vocabulary for the complete tag and attribute sets, value categories, open-ended name families, and rejected React-spelling suggestions.
SVG elements use a finite v1 subset: svg, g, path, circle, ellipse, rect, line, polyline, polygon, text, tspan, defs, symbol, use, clipPath, mask, linearGradient, radialGradient, stop, title, desc, foreignObject, pattern, filter, marker, and image. Supported SVG spellings include viewBox, preserveAspectRatio, xlink:href, xml:lang, stroke-width, fill-rule, and stroke-linecap. The namespace begins at svg. Children inside foreignObject use HTML rules, and a nested svg switches back to SVG. Unsupported SVG tags or attributes fail by name.
Styles and events
The style prop accepts a CSS string or a plain object with supported CSS-spelled keys. CSSProperties is exported from @takazudo/. Nonzero lengths must include their units as strings; the runtime never adds px. Numeric zero remains valid for lengths, unitless properties such as opacity and line-height accept numbers, and custom properties accept finite numbers or strings. The runtime applies the same checks during SSR, hydration, mount, and updates.
Use a plain object literal for object styles. Arrays, class instances, null-prototype records, nested objects, camelCase keys, and reactive values inside the object are rejected. Null and undefined entries are skipped, entries keep their insertion order, and an update removes properties omitted from the next object. The whole style prop may be reactive.
<div
style={{
left: "1rem",
top: 0,
width: "100%",
"max-height": "20rem",
opacity: 0.5,
"--scale": 2,
"-webkit-mask-image": "linear-gradient(black, transparent)",
}}
/>
<iframe title="Preview" style={{ width: "100%", height: "480px" }} />A CSS string is an escape hatch for valid CSS syntax that does not fit the checked object form. Its contents are emitted as authored and are not checked for supported property names or values:
<div style="left: calc(1rem + var(--offset));" />Function listeners use a native, case-sensitive event name after on:, with an optional final :capture suffix:
<button on:click={save} on:keydown:capture={handleKey}>Save</button>For a known on:<name> prop, the handler receives that name's native event type: on:submit receives SubmitEvent, and on:keydown receives KeyboardEvent. Its currentTarget is typed as the element carrying the prop; target is not refined. Unknown or custom event names accept any single-event handler. currentTarget is available only while the event is being dispatched, so capture the element before an await if the handler needs it afterward. There is no synthetic event system or event delegation. The runtime removes listeners with their owning scope. A rejected promise returned by a listener is reported, including if the scope is later disposed.
Lowercase string event attributes such as onload are a separate inline-handler form. They accept strings, including reactive string values; null and undefined omit the attribute. The renderer escapes the text and writes the browser's inline-handler attribute, but does not turn it into a zudo-react listener. The browser may execute that text when the event fires, so use trusted strings. Do not supply both an inline attribute and a function listener for the same event. React-style onClick and function-valued onclick are rejected; use on:click for a function listener.
Refs are simple objects with a current field, initially null. The runtime assigns all refs before activation callbacks. There are no callback refs or ref factories.
Iframe shells
Inside an island, zudo-react owns a childless <iframe> element, its supported attributes, refs, and native listeners. The browser owns the foreign document loaded through src or srcdoc; zudo-react does not reconcile that document. Keep the shell childless: JSX children and rawHtml are rejected, and hydration fails if nonempty fallback DOM appears in the iframe element. An unchanged hydrate adopts the existing iframe and document. Updating src or srcdoc asks the browser to navigate that document.
srcdoc is trusted application input. zudo-react escapes it for the outer HTML attribute but does not sanitize the document. Keep it under application control, and choose sandbox and allow for the embedded content.
<iframe title="HTML preview" srcdoc={trustedDocument} sandbox="" allow="fullscreen" />Custom elements
A custom element name must be lowercase and contain a hyphen. Its attributes must be strings, with reactive strings or nullish values also allowed. Native listeners and reserved runtime props are supported. The runtime does not assign arbitrary objects to element properties or infer property types.
Custom element constructors can run browser code when the browser creates an element; the runtime cannot undo those external effects if rendering later fails.
Trusted raw HTML
Use rawHtml only when the caller already trusts the string. It accepts a string or a reactive string and is mutually exclusive with children, even when children is explicitly empty. It is a prop for the renderer, never an HTML attribute. Raw HTML is unsupported in SVG.
const markup = signal("<em>trusted</em>");
return <section rawHtml={markup} />;In an island, raw HTML occupies one opaque region. Hydration checks its boundaries but does not inspect or bind the contents. A changed string replaces only the contents of that region. The caller is responsible for valid markup, trust, and any external resources it adds. Reserved zudo-react comment shapes and nested island wrappers are rejected. Scripts inserted by a client rawHtml update are inert.
For a pre whose rendered children begin with a newline, the renderer adds a compensating newline so HTML parsing preserves the intended first newline. A rawHtml payload is emitted as-is, without that compensation.
Inside script and style, children are rejected. Inline raw text requires a static string in rawHtml; a childless external script may instead use src. Reactive payloads and internal comment markers are not supported. A case-insensitive closing script or style sequence for the current element is rejected. Script payloads are also rejected when HTML parsing would consume the renderer's closing </script> tag, such as <!--<script>. Harmless forms including <!--, <!-- <script> -->, and ordinary <script> text remain accepted. This check follows the HTML script tokenizer, including its escaped and double-escaped states; style uses RAWTEXT rules. Rejected payloads report ZR_RAW_HTML in server and client rendering. The runtime does not escape arbitrary JavaScript or CSS. Island props travel in attributes, not executable script blocks.
For JSON data transport in a direct <script type="application/json"> host, serialize a JSON-compatible object first, then escape < and the reserved marker substrings in the serialized JSON. Escaping each marker's first character as a JSON Unicode escape preserves its original case and the parsed value, including literal backslash-u text. This is for data transport, not a sanitizer or a serializer for executable JavaScript:
function jsonForRawHtml(value: unknown): string {
const json = JSON.stringify(value);
if (json === undefined) throw new TypeError("Expected a JSON-compatible object");
return json
.replace(/</g, "\\u003c")
.replace(/data-zfb-island|zr:1:/gi, (marker) =>
`\\u${marker.charCodeAt(0).toString(16).padStart(4, "0")}${marker.slice(1)}`,
);
}
const data = { label: 'DATA-zfb-IsLaNd', text: '</script>' };
return <script type="application/json" rawHtml={jsonForRawHtml(data)} />;The script's text remains JSON: JSON.parse(script.textContent) recovers data exactly. A <script type="application/json"> nested inside another element's rawHtml is a different case: the outer payload uses a conservative, case-insensitive substring fallback for marker-bearing JSON. Encode it with the same recipe before embedding it. Actual reserved attributes, protocol comments, and payloads that consume renderer boundaries remain rejected.
Move text children from an inline style element into rawHtml:
export function CriticalStyles() {
const criticalCss = `body { font-family: "Atkinson", sans-serif; }`;
return <style rawHtml={criticalCss} />;
}An inline script uses the same form, while an external script remains a childless element:
export function InlineBootstrap() {
const bootstrap = "window.appReady = true;";
return <script rawHtml={bootstrap} />;
}
export function ExternalScript() {
return <script src="/assets/app.js" />;
}For script and style, the string is emitted verbatim, including quotes, ampersands, and < characters; rawHtml does not escape or sanitize JavaScript or CSS. Keep these values trusted and do not include the closing tag for the current element. This no-children rule is specific to script and style: title and textarea still render their ordinary text children, and ordinary elements still accept children (or rawHtml without children).