Reference: JSX

Reference

JSX

Sigil uses JSX with HTML attribute names and lowercase event handlers. There is no virtual DOM: each element compiles to a real DOM node and an update function that touches only the nodes whose values changed.

For the reactivity macros themselves, see Reactivity macros.

Expressions

Any JavaScript expression in {} is re-evaluated when the state it reads changes:

<>
	<p>{user.name} has {books.length} books</p>
	<img src={cover} alt={`Cover of ${title}`} />
</>

Reading reactive state inside an expression is what subscribes it. A read outside an expression — in an event handler, a plain function — does not.

Events

Event props are on plus the lowercase event name:

<>
	<button onClick={() => count++}>Add</button>
	<form onSubmit={save}></form>
</>

onClick becomes addEventListener("click", …). The on prefix must be followed by a capital letter — onclick is treated as a plain attribute and becomes an HTML onclick string, not a handler.

Reach for bind: first

If an event exists only to copy a value into state, don't write the event. bind: says what you mean:

// ✗ two-way by hand, and you will forget the write-back half
const manual = <input value={name} onInput={(e) => (name = e.currentTarget.value)} />;

// ✓
const bound = <input bind:value={name} />;

onInput is not wrong, it is just more to write and more to get wrong. The manual version is the escape hatch for when you need a handler that does something other than copy — debounce, validate, transform:

<>
	<input
		value={query}
		onInput={(e) => {
			query = e.currentTarget.value;
			scheduleSearch();
		}}
	/>
</>

Same for forms: prefer an action with enhance over a fetch in a submit handler. See form actions.

Fragments

<>
	<dt>{term}</dt>
	<dd>{definition}</dd>
</>

A fragment groups without adding a node. Returning several elements from a component does not need one, but returning siblings inside JSX does.

Components

A component is a function returning JSX. The file's default export is the page; elsewhere, name and import it:

import { Button } from "../components/Button";

const Save = () => <Button kind="primary">Save</Button>;

Props are one object. children is the JSX between the tags:

const Panel = ({ title, children }) => (
	<section>
		<h2>{title}</h2>
		{children}
	</section>
);

See tutorial step 3 for the full version with scoped styles.

Lists

.map inside JSX. Give each item a stable key — the reconciler matches rows by key, so a new item gets a new node, a removed one loses its node, and a reorder moves the existing nodes instead of rebuilding them:

<>
	<ul>
		{books.map((book) => (
			<li key={book.id}>{book.title}</li>
		))}
	</ul>
</>

Use a stable id from your data, never the array index. With an index as the key, a reorder looks like a wholesale replace: every node is torn down and rebuilt, losing focus and any state attached to those elements.

Attributes

Sigil uses HTML names: class, not className; for, not htmlFor. Dashed attributes are written literally:

<>
	<input type="checkbox" aria-label="Done" data-id={id} />
</>

Attributes whose value is null or undefined are omitted, and a boolean false omits the attribute — which is how you make an attribute conditional:

<>
	<input disabled={!valid} />
</>

The camelCase trap

className and htmlFor are translated for you, so they work. Almost nothing else is. A camelCase attribute is passed through to the DOM as-is:

WrittenRenderedWorks?
classclassyes — the correct spelling
classNameclassyes — translated
for / htmlForforyes
aria-labelaria-labelyes — dashed, literal
ariaLabelariaLabelno — invalid attribute, silently ignored
tabIndextabIndexpartly — see below

HTML attribute names are case-insensitive, so tabIndex happens to be parsed as tabindex and lands correctly. But the compiler's map only holds class and for, so nothing tells you which camelCase names are safe. Assume none of them are. Write aria-label, not ariaLabel. A silently-ignored accessibility attribute is the kind of bug that never shows up in a test and always shows up for someone using a screen reader.

The rule: dashed for anything with a dash, lowercase for everything else. The only two camelCase names worth typing are className and htmlFor, and you shouldn't be typing those either.

Styles

A style prop takes an object, and the keys are CSS properties:

<>
	<div style={{ opacity: visible ? 1 : 0, "--w": `${n}px` }}>…</div>
</>

Values are numbers for unitless properties, strings with units otherwise. Custom properties go in as-is.

Spreads

<>
	<div {...props} />
</>

Applied per key, so later attributes win. The compiler also knows what a spread will produce when rendering to HTML on the server, so the first paint matches.

Raw HTML

innerHTML takes a SafeHtml, not a plain string — the type forces you to say where the content came from:

import { SafeHtml } from "@sigil-dev/runtime";

<div innerHTML={new SafeHtml(userBio)} />

A bare string is a type error on purpose. Untrusted content in a SafeHtml is an XSS hole; the constructor is where that decision gets made deliberately.

Directives

use attaches a function to an element, given either a function or [fn, params]:

import { enhance } from "@sigil-dev/grimoire/client";

<form method="POST" use={[enhance, { action: "?/create" }]}></form>

Each runs on mount and returns a cleanup function. Directives and transitions covers the pattern; enhance is documented under form actions.

Binding

bind: assigns an element or its value to a signal, replacing a ref plus an event listener:

let name = $state("");
let done = $state(false);
let panel = $state(null);

const form = (
	<>
		<input bind:value={name} />
		<input type="checkbox" bind:checked={done} />
		<div bind:this={panel}></div>
	</>
);

bind:value replaces the value + onInput pair: it writes the input into the signal as you type, and writes the signal back into the input when it changes, which is how name = "" clears the box. bind:checked is the same for a checkbox, and it works on a property — ticking the box sets book.read on that object. bind:this gives you the node. On a radio group, bind:group collects the checked value.

Server rendering

The same JSX compiles twice — once to DOM code for the browser, once to an HTML string for the server — and the server's output is what the browser adopts at hydration. Attributes are rendered with HTML semantics: class stays class, booleans become bare attributes, and a <select value> marks the matching <option selected>. See The compiler and Hydration.

Next

Edit this page