Svelte tutorial

Lily Svelte library na di canonical implementation: Svelte 5 runes, typed props, rest-prop spread, and two-way binding with bind:value.

Wetin you go build

Small contact form — input wey get label, submit button, two-way binding, real keyboard and screen-reader behaviour — first bare, then styled, for about 10 minutes.

Before you start

You need git, Node 22 or newer, and pnpm 10 or newer. Any Svelte 5 project dey work — fresh npx sv create app go do.

Step 1 — Get di 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

Di components dey live inside src/lib/components/{Name}/, one directory for each component wey get di .svelte file, im tests, im stories, and im docs side by side. Copy di directories wey you need enter your app, or depend on di repo and import directly.

Step 2 — Your first component

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

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

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

E dey render <button class="button" type="button">. Di props dey typed: type, disabled, pressed (e dey render aria-pressed for toggle buttons), label (an aria-label override), and any extra attribute go spread onto di root.

Step 3 — Compose small 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 dey take di visible label as im own prop instead of nested label component — pass am once, on Field, and TextInputfor label dey provide di accessible name. TextInput's value na $bindable rune prop, so bind:value dey give you two-way binding straight away.

Verified 2026-08-29: dis exact snippet dem compile through svelte/server and check am for single, correctly linked <label for> — earlier draft nest separate Label inside Field, wey render two labels (one empty) because Field already dey render im own from di label prop. Di contact-form composed route na di same pattern, wey real e2e and axe tests don exercise.

Check your work: run di app and di form go look plain — exactly as dem plan am: di styling still dey come. Press Tab: focus go move through every control, di label go dey announced, and submit dey work. Di behaviour don finish; di look na yours and e be next.

Step 4 — Style am

Everything above dey render as pure structure, ready for your styles. Style di class hooks — .button, .text-input, .label, .field — inside your global stylesheet, or link ready-made theme inside app.html:

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

Per-instance tweaks dey go through di class prop, wey dem append to di base hook: <Button class="wide"> dey render class="button wide".

Step 5 — See everything dey work

Di SvelteKit example app na di same catalog fully styled, with live demos, Playwright e2e tests, and 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

Next steps

You get questions as you dey go? Di help page full of answers — and if any step fit clearer, tell us and we go gladly improve di tutorial.