Tutorial: 1. State and events

Tutorial

1. State and events

In this step you'll build the first version of the page: a title box, an Add button, and a count of books added. It covers the core of Sigil: state that updates the page by itself.

Replace the starter page

Open src/routes/index.tsx and replace it:

src/routes/index.tsx
let title = $state("");
let saved = $state(0);
let tooLong = $derived(title.length > 60);

const Index = () => (
	<main>
		<h1>Reading list</h1>
		<input
			placeholder="Book title"
			value={title}
			onInput={(e) => (title = e.currentTarget.value)}
		/>
		<button
			disabled={title === "" || tooLong}
			onClick={() => {
				saved++;
				title = "";
			}}
		>
			Add
		</button>
		{tooLong && <p class="warning">Titles stop at 60 characters.</p>}
		<p>
			You've added {saved} {saved === 1 ? "book" : "books"}.
		</p>
	</main>
);

export default Index;

Save, and the browser reloads. Type in the box: the button enables itself. Type past 60 characters and the warning appears. Press Add and the count goes up.

What's happening

$state makes a value reactive. title and saved are ordinary variables. You read them and assign to them like any let. The difference is that the compiler tracks where they're used, so saved++ updates every place on the page that shows saved.

$derived is a value computed from other state. tooLong recalculates whenever title changes. You never assign to it.

Events are on + the event name. onClick, onInput, onSubmit: the handler receives the DOM event. e.currentTarget is the element the handler is on.

{condition && <jsx />} shows or hides markup. When tooLong becomes true, the paragraph is created and inserted; when it goes false, it's removed. A ternary works the same way.

Two things are missing here that you might expect from other frameworks. There are no imports: $state and $derived are compiler macros, not functions. And the component doesn't re-render. Index runs once. After that, each expression that reads state ({saved}, disabled={…}, the conditional) updates on its own. The compiler page shows the code this turns into.

Keeping the input in sync

The input has two halves: value={title} writes state into the box, and onInput writes the box back into state. That's why title = "" after adding also clears the box. The next step replaces this pair with a single bind:value.

Next: lists and bindings.

Edit this page