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:
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.