Svelte tutorial

Lily's Svelte library is the canonical implementation: Svelte 5 runes, typed props, rest-prop spreid, an two-wey binding wi bind:value.

Whit ye'll big

A wee contact form — labelled input, submit button, two-wey binding, real keyboard an screen-reader behaviour — first bare, then styled, in aboot 10 minutes.

Afore ye stairt

Ye need git, Node 22 or later, an pnpm 10 or later. Onie Svelte 5 project wirks — a fresh npx sv create app is fine.

Step 1 — Git the code

git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-svelte-headless
pnpm install
pnpm test        # vitest — thousands of component cases
pnpm run storybook   # browse all 571 components

The components live in src/lib/components/{Name}/, ae directory per component wi the .svelte file, its tests, its stories, an its docs side bi side. Copy the directories ye need intae yer app, or depend on the repo an import directly.

Step 2 — Yer first component

<script>
  import Button from "#lib/components/Button/Button.svelte";

  function save() { console.log("saved"); }
</script>

<Button onclick={save}>Save</Button>

Renders <button class="button" type="button">. The props are typed: type, disabled, pressed (renders aria-pressed for toggle buttons), label (an aria-label owerride), an onie extra attribute spreids ontae the root.

Step 3 — Compose a wee form

<script>
  import Form from "#lib/components/Form/Form.svelte";
  import Field from "#lib/components/Field/Field.svelte";
  import TextInput from "#lib/components/TextInput/TextInput.svelte";
  import Button from "#lib/components/Button/Button.svelte";

  let name = $state("");
</script>

<Form label="Contact form" onsubmit={handleSubmit}>
  <Field label="Full name" required>
    <TextInput id="name" label="Full name" bind:value={name} required />
  </Field>
  <Button type="submit">Save</Button>
</Form>

Field taks the visible label as its ain prop raither than a nestit label component — pass it ance, on Field, an TextInputon label supplies the accessible name. TextInput's value is a $bindable rune prop, sae bind:value gies ye two-wey binding straucht awa.

Verified 2026-08-29: this exact snippet wis compilit through svelte/server an checked for a single, correctly linkit <label for> — an earlier draft nestit a separate Label inside Field, which renderit twa labels (ane empty) because Field aareadies renders its ain frae the label prop. The contact-form composed route is the same pattern, exercised bi real e2e an axe tests.

Check yer wirk: rin the app an the form looks plain — exactly as intendit: styling is still aheid. Press Tab: focus muives through ilka control, the label is announced, an submit wirks. The behaviour is deen; the look is yours, an it's neist.

Step 4 — Style it

Aathing abune renders as pure structure, ready for yer styles. Style the class hooks — .button, .text-input, .label, .field — in yer global stylesheet, or link a ready-made theme in app.html:

<link rel="stylesheet" href="/themes/dark.css" />

Per-instance tweaks gang through the class prop, which is appendit tae the base hook: <Button class="wide"> renders class="button wide".

Step 5 — See it aw wirkin

The SvelteKit example app is the same catalog fully styled, wi live demos, Playwright e2e tests, an an axe-core accessibility baseline:

git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-svelte-sveltekit-examples
pnpm install && pnpm run dev

Neist steps

Questions alang the wey? The help page is fu o answers — an if a step could be clearer, tell us an we'll gladly improve the tutorial.