Component catalog

Every standard element, running

67 elements and 6 widgets, all themed with CSS variables, all usable by keyboard, all in light and dark. Import from $lib/ui; register widgets in $lib/widgets.

Every one of them is free. No pro tier, no paid blocks, no dependency beyond Svelte: the whole kit ships MIT-licensed with NebulaKit.

Actions

Button

Four variants and three sizes. With an href it renders a link; loading keeps focus and blocks clicks.

A link
Code
import { Button } from '$lib/ui';

<Button on:click={save}>Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost" size="sm">Skip</Button>
<Button variant="danger" loading={deleting}>Delete</Button>
<Button href="/documentation">Read the docs</Button>

Command

A command palette: search ranks label and keyword matches as you type. Inline, or in a modal on ⌘K.

New project ⌘N
Import from GitHub
Deploy to production
Settings ⌘,
Billing
Invite teammates
Toggle theme
Open documentation

New project ⌘N
Import from GitHub
Deploy to production
Settings ⌘,
Billing
Invite teammates
Toggle theme
Open documentation

Code
import { Command } from '$lib/ui';

import { isCommandShortcut } from '$lib/ui/overlay-logic';

<svelte:window on:keydown={(e) => {
  if (isCommandShortcut(e)) { e.preventDefault(); paletteOpen = true; }
}} />

<Command
  dialog
  bind:open={paletteOpen}
  shortcut="⌘K"
  items={[
    { id: 'new', label: 'New project', group: 'Projects', shortcut: '⌘N' },
    { id: 'settings', label: 'Settings', group: 'Account', keywords: ['preferences'] }
  ]}
  on:select={(e) => run(e.detail.id)}
/>

Toggle

A button that stays pressed, like Bold in a toolbar. Says so with aria-pressed.

Code
import { Toggle } from '$lib/ui';

<Toggle bind:pressed={bold}>Bold</Toggle>
<Toggle bind:pressed={pinned} label="Pin" variant="outline" size="sm">📌</Toggle>

ToggleGroup

A row of toggles with one tab stop. Single is a radio group; multiple is a set of pressed buttons.

Aligned left; styles: b.

Code
import { ToggleGroup } from '$lib/ui';

<ToggleGroup label="Alignment" bind:value={align}
  items={[{ value: 'left', label: 'Left' }, { value: 'center', label: 'Centre' }]} />
<ToggleGroup label="Style" type="multiple" bind:value={styles}
  items={[{ value: 'b', label: 'Bold' }, { value: 'i', label: 'Italic' }]} />

ButtonGroup

Joins buttons into one control with shared borders. A labelled group, across or down.

Code
import { ButtonGroup, Button } from '$lib/ui';

<ButtonGroup label="Text alignment">
  <Button variant="secondary">Left</Button>
  <Button variant="secondary">Centre</Button>
  <Button variant="secondary">Right</Button>
</ButtonGroup>

Forms

TextInput

A labelled input with a hint and an error, wired with aria-describedby and aria-invalid.

Type something without an @ to see the error.

Code
import { TextInput } from '$lib/ui';

<TextInput label="Email" type="email" bind:value={email}
  hint="We never share it." error={emailError} required />

Textarea

Multi-line input, with a live character count when it has a maxlength.

0 / 160

Code
import { Textarea } from '$lib/ui';

<Textarea label="Bio" bind:value={bio} maxlength={160} />

Select

A labelled native select, the most accessible picker there is.

Code
import { Select } from '$lib/ui';

<Select label="Plan" bind:value={plan} placeholder="Choose a plan"
  options={[{ value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }]} />

Checkbox

A box and its label, the whole row clickable. Supports indeterminate.

Code
import { Checkbox } from '$lib/ui';

<Checkbox label="Email me updates" hint="About once a month." bind:checked />

RadioGroup

One choice from a few, as a fieldset with a legend.

Billing
Code
import { RadioGroup } from '$lib/ui';

<RadioGroup legend="Billing" bind:value={billing} inline
  options={[{ value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' }]} />

Switch

An on/off setting that applies at once. A real checkbox with role="switch".

Code
import { Switch } from '$lib/ui';

<Switch label="Notifications" bind:checked={notify} />

Slider

A labelled range that shows its value, and announces it with the unit.

40%
Code
import { Slider } from '$lib/ui';

<Slider label="Volume" bind:value={volume} unit="%" />

Field

The label, hint and error around any control. Every form control is built on it.

Any colour; this one is native.

Code
import { Field } from '$lib/ui';

<Field label="Colour" hint="Any CSS colour" let:id let:describedBy>
  <input {id} type="color" aria-describedby={describedBy} />
</Field>

Combobox

Type to filter a list, arrow to an option, Enter to pick. Accent-insensitive; can create new values.

Accents do not matter. Type a new city and press Enter to add it.

Chosen: nothing yet

Code
import { Combobox } from '$lib/ui';

<Combobox label="Timezone" bind:value={zone} creatable
  options={[{ value: 'Europe/Berlin', label: 'Berlin' }, { value: 'Europe/Zurich', label: 'Zürich' }]}
  on:create={(e) => add(e.detail.value)} />

Calendar

A month as a keyboard grid: arrows by day and week, Page keys by month and year. Localized by Intl.

October 2026
MonTueWedThuFriSatSun
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

Pick a weekday. Try PageDown, and Shift+PageDown.

Code
import { Calendar } from '$lib/ui';

<Calendar bind:value={day} min="2026-01-01" weekStartsOn={1} locale="en-GB"
  isDisabled={(iso) => [0, 6].includes(new Date(iso).getUTCDay())} />

DatePicker

A date field you can type into, with a calendar in a panel. Escape closes and returns focus.

Type yyyy-mm-dd, or open the calendar.

Code
import { DatePicker } from '$lib/ui';

<DatePicker label="Start date" bind:value={start} min={today} hint="yyyy-mm-dd" />

InputOTP

A one-time code in single-character cells. Auto-advances, pastes whole codes, offers SMS autofill.

Verification code

We sent a code to your phone. Try 123456, or paste it.

Code
import { InputOTP } from '$lib/ui';

<InputOTP bind:value={code} length={6} on:complete={(e) => verify(e.detail.value)} />

InputGroup

An input joined to text, icon or button addons. Text addons are read with the input.

https:// .com
Code
import { InputGroup, Button } from '$lib/ui';

<InputGroup label="Website" prefix="https://" suffix=".com" bind:value={site} />
<InputGroup label="Search" type="search" bind:value={q}>
  <Button slot="trailing" size="sm">Go</Button>
</InputGroup>

NumberInput

A spinbutton with − and + buttons. Arrow and Page keys step; typed values snap to the step.

Between 1 and 12.

Code
import { NumberInput } from '$lib/ui';

<NumberInput label="Guests" bind:value={guests} min={1} max={12} />
<NumberInput label="Weight" bind:value={kg} step={0.5} unit="kg" />

TagInput

Type and press Enter or a comma to add chips. Deduplicates, caps the count, and validates.

  • svelte
  • accessibility

Enter or a comma adds; Backspace removes the last. Up to five.

Code
import { TagInput } from '$lib/ui';

<TagInput label="Topics" bind:tags max={5}
  validate={(t) => (t.length > 20 ? 'Keep it under 20 characters.' : '')} />

FileDrop

A dropzone that is also a button. Checks type and size itself, and says why a file was refused.

Attachments

Try a .txt file, or something over 5 MB, to see a refusal.

Code
import { FileDrop } from '$lib/ui';

<FileDrop label="Attachments" bind:files accept="image/*,.pdf" maxSize={5 * 1024 * 1024}
  multiple on:reject={(e) => console.warn(e.detail.rejections)} />

Feedback

Alert

A message in the page. Danger and warning interrupt a screen reader; info and success do not.

Heads up

A new version is available.
Your changes were saved.
Code
import { Alert } from '$lib/ui';

<Alert tone="warning" title="Trial ends soon" dismissible>Three days left.</Alert>

Toaster

Brief messages from anywhere. One <Toaster /> in the layout; raise them with toast.*().

Code
import { Toaster, toast } from '$lib/ui';

<!-- once, in +layout.svelte -->
<Toaster />

toast.success('Saved');
toast.danger('Could not reach the server', 0); // 0 = stays until dismissed

Badge

A short status label in five tones, readable in both themes.

Draft Preview Live Degraded Failed
Code
import { Badge } from '$lib/ui';

<Badge tone="success">Live</Badge>

Progress

How far along something is. Leave out the value for work of unknown length.

Uploading 40%
Quota 92%
Working on it
Code
import { Progress } from '$lib/ui';

<Progress label="Uploading" value={uploaded} max={total} />

Spinner

Indeterminate activity, announced as a status.

Loading Loading Loading the report
Code
import { Spinner } from '$lib/ui';

<Spinner label="Saving" />

Skeleton

Placeholder shapes while content loads. Still under reduced motion.

Code
import { Skeleton } from '$lib/ui';

<Skeleton shape="circle" height="3rem" />
<Skeleton lines={3} />

Overlays

Dialog

A modal on the native <dialog>: focus trap, Escape and the backdrop for free.

Delete project?

The project and its deploys go for good. This cannot be undone.

Code
import { Dialog, Button } from '$lib/ui';

<Dialog bind:open title="Delete project?" description="This cannot be undone.">
  <svelte:fragment slot="actions">
    <Button variant="secondary" on:click={() => (open = false)}>Cancel</Button>
    <Button variant="danger" on:click={remove}>Delete</Button>
  </svelte:fragment>
</Dialog>

Tooltip

A short description on hover and focus, read with the control. Escape hides it.

Copies the link to your clipboard Below, for controls near the top of the page
Code
import { Tooltip } from '$lib/ui';

<Tooltip text="Copies the link"><button>Copy</button></Tooltip>

Popover

A trigger and a floating panel for small forms and details. Flips at the screen edge; Escape returns focus.

Code
import { Popover, Button, TextInput } from '$lib/ui';

<Popover title="Share" placement="bottom" align="start" bind:open>
  <Button slot="trigger" variant="secondary">Share</Button>
  <TextInput label="Link" value={link} readonly />
  <Button size="sm" on:click={copy}>Copy link</Button>
</Popover>

HoverCard

A rich preview on hover and on keyboard focus, with open and close delays. Never the only way to the content.

Built by @ada — hover or tab to the name.

Code
import { HoverCard } from '$lib/ui';

<HoverCard openDelay={500} closeDelay={200}>
  <a slot="trigger" href="/people/ada">@ada</a>
  <strong>Ada Lovelace</strong>
  <p>Wrote the first published program.</p>
</HoverCard>

Sheet

A modal panel that slides in from any edge: a side panel, or a bottom drawer on a phone. Native <dialog>.

Filters

Narrow the project list. Changes apply when you press Apply.

Share to…

Code
import { Sheet, Button } from '$lib/ui';

<Sheet bind:open side="right" title="Filters" description="Narrow the list.">
  …
  <svelte:fragment slot="actions">
    <Button variant="secondary" on:click={() => (open = false)}>Close</Button>
    <Button on:click={apply}>Apply</Button>
  </svelte:fragment>
</Sheet>

AlertDialog

A confirmation that asks before it acts. Cancel takes focus first, and a backdrop click does not dismiss it.

Delete “Aurora”?

The project, its deploys and its domains go for good. This cannot be undone.

Code
import { AlertDialog } from '$lib/ui';

<AlertDialog
  bind:open
  title="Delete project?"
  description="The project and its deploys go for good."
  confirmLabel="Delete"
  tone="danger"
  on:confirm={remove}
/>

ContextMenu

A right-click menu at the pointer, also on Shift+F10. Held inside the screen; offer the same actions visibly too.

Code
import { ContextMenu } from '$lib/ui';

<ContextMenu
  label="File actions"
  items={[
    { id: 'open', label: 'Open', shortcut: '↵' },
    { id: 'rename', label: 'Rename', shortcut: 'F2' },
    { separator: true },
    { id: 'delete', label: 'Delete', danger: true }
  ]}
  on:select={(e) => run(e.detail.id)}
>
  <button class="file">report.pdf</button>
</ContextMenu>

Layout

Container

The page-width wrapper: a max width from sm to xl, with gutters that grow on wider screens.

A sm container: 40rem at most, centred, with gutters that widen on bigger screens.

Code
import { Container } from '$lib/ui';

<Container size="md" as="main">…</Container>

Stack

A flex row or column with a gap token, align, justify and wrap. The everyday layout.

Projects
SvelteWorkersD1KVR2
Code
import { Stack, Button } from '$lib/ui';

<Stack direction="row" gap="sm" justify="between" align="center" wrap>
  <h2>Projects</h2>
  <Button>New project</Button>
</Stack>

Separator

A line between groups, either way. Decorative by default, or a real separator with a label.

Signed in as [email protected]

Code
import { Separator } from '$lib/ui';

<Separator />
<Separator label="or" decorative={false} />
<Separator orientation="vertical" />

AspectRatio

Holds media at a fixed shape so the page does not jump while it loads.

16 : 9
Code
import { AspectRatio } from '$lib/ui';

<AspectRatio ratio="16/9">
  <img src={cover} alt="The launch event" />
</AspectRatio>

ScrollArea

A labelled, keyboard-scrollable region with thin themed scrollbars and shadows only where there is more.

2.4.0

Command palette: fuzzy search across every page and action.

2.3.2

Fixed the dark theme’s focus ring on outline buttons.

2.3.0

Widgets can now be resized from the keyboard.

2.2.1

Faster cold starts on Cloudflare Workers.

2.2.0

Toasts stack and pause while hovered.

2.1.0

Tables sort by any column, numbers as numbers.

2.0.0

A new UI kit, themed entirely with CSS variables.

Code
import { ScrollArea } from '$lib/ui';

<ScrollArea label="Release notes" maxHeight="16rem">
  {#each notes as note}<p>{note}</p>{/each}
</ScrollArea>

Resizable

Two panes and a divider to drag, or to move with the arrow keys. Enter collapses and restores.

Files

src/
routes/
lib/

Editor

Drag the divider, or focus it and use the arrow keys.

File list: 35%

Code
import { Resizable } from '$lib/ui';

<Resizable bind:size min={20} max={80}>
  <nav slot="first">Files</nav>
  <section slot="second">Editor</section>
</Resizable>

Collapsible

One region shown and hidden by a button. Eases open, or snaps under reduced motion.

Called on deploy.

Closed

Code
import { Collapsible } from '$lib/ui';

<Collapsible label="Show advanced options" bind:open>
  <TextInput label="Webhook URL" bind:value={webhook} />
</Collapsible>

Data display

Card

A surface with an optional header and footer. With an href the whole card is a link.

Pro plan

Popular
Everything in Free, plus custom domains and priority support.

A whole-card link

The entire card is one link target.
Code
import { Card, Button } from '$lib/ui';

<Card title="Pro plan">
  Everything in Free, plus custom domains.
  <svelte:fragment slot="footer"><Button>Upgrade</Button></svelte:fragment>
</Card>

Table

Rows and columns with optional sorting. Wide tables scroll in their own frame.

Recent deploys
main Live 2.4
feature/search Preview 3.1
fix/login Failed 0.8
docs Preview 1.2
Code
import { Table } from '$lib/ui';

<Table caption="Deploys" {rows} columns={[
  { key: 'branch', label: 'Branch', sortable: true },
  { key: 'minutes', label: 'Minutes', sortable: true, align: 'end' }
]} />

Avatar

A picture, or initials when there is none or it fails to load.

Code
import { Avatar } from '$lib/ui';

<Avatar name="Ada Lovelace" src={user.image} />

Kbd

A key or a chord of keys.

Ctrl K ⌘ Shift P Esc
Code
import { Kbd } from '$lib/ui';

<Kbd keys={['Ctrl', 'K']} />

EmptyState

What an empty list shows, and what to do next.

No projects yet

Projects you create show up here.

Code
import { EmptyState, Button } from '$lib/ui';

<EmptyState icon="✨" title="No projects yet" description="Make the first one.">
  <Button>New project</Button>
</EmptyState>

DataTable

Search, sort, page and select. Select-all goes mixed for a partial page, and the result count is announced.

Team
Ada Lovelace Engineering lead Active 1,284
Alan Turing Research Active 642
Grace Hopper Compilers Away 2,210
Edsger Dijkstra Algorithms Active 98
Barbara Liskov Architecture Active 873
Code
import { DataTable, Button } from '$lib/ui';

<DataTable caption="Team" {rows} rowKey="id" selectable bind:selected
  columns={[
    { key: 'name', label: 'Name', sortable: true },
    { key: 'role', label: 'Role' },
    { key: 'commits', label: 'Commits', sortable: true, align: 'end',
      format: (v) => Number(v).toLocaleString() }
  ]}
  on:selection={(e) => (picked = e.detail.selected)}>
  <svelte:fragment slot="toolbar" let:selected>
    <Button size="sm" variant="danger" disabled={!selected.length}>Remove</Button>
  </svelte:fragment>
</DataTable>

BarChart

Grouped bars in plain SVG on round ticks. Screen readers get a summary and the numbers as a table.

Deploys per day
051015 Mon Tue Wed Thu Fri
Deploys per day
Category ProductionPreview
Mon 412
Tue 79
Wed 514
Thu 911
Fri 68
Code
import { BarChart } from '$lib/ui';

<BarChart title="Deploys per day" labels={['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
  series={[
    { name: 'Production', values: [4, 7, 5, 9, 6] },
    { name: 'Preview', values: [12, 9, 14, 11, 8] }
  ]} />

LineChart

One or more lines, with an optional area fill. A null value breaks the line. Arrow keys step through points.

Visitors
0200400600800MonTueWedThuFriSatSun
Visitors
Point This weekLast week
Mon 320280
Tue 410300
Wed 380—
Thu 520410
Fri 610470
Sat 450400
Sun 390350
Code
import { LineChart } from '$lib/ui';

<LineChart title="Visitors" area labels={days}
  series={[
    { name: 'This week', values: [320, 410, 380, 520, 610, 450, 390] },
    { name: 'Last week', values: [280, 300, null, 410, 470, 400, 350] }
  ]} />

DonutChart

Parts of a whole, with the total in the middle and each share in the legend.

Traffic sources
10,600 Visits
Traffic sources
PartValueShare
Search 5,200 49%
Direct 3,100 29%
Social 1,400 13%
Referral 900 8%
Code
import { DonutChart } from '$lib/ui';

<DonutChart title="Traffic sources" data={[
  { label: 'Search', value: 5200 },
  { label: 'Direct', value: 3100 },
  { label: 'Social', value: 1400 }
]} />

Item

A list row: media, title, description, meta and actions. With an href the whole row is the link.

Weekly digest

A summary of the week, every Monday.

Storage

8.2 / 10 GB
Code
import { Item, Avatar, Button } from '$lib/ui';

<Item title="Ada Lovelace" description="Reviewed your pull request" meta="2h" href="/people/ada">
  <Avatar slot="media" name="Ada Lovelace" size="sm" />
  <Button slot="actions" size="sm" variant="ghost">Reply</Button>
</Item>

Timeline

Events in order on a line, each with a real <time> and a dot coloured by tone.

  1. Deployed to production

    v2.4.0 — the data-display kit.

  2. Preview checks failed

    One flaky test, re-run and passed.

  3. Release branch cut

  4. Planning

Code
import { Timeline } from '$lib/ui';

<Timeline label="Deploy history" items={[
  { time: '9:41', datetime: '2026-10-03T09:41', title: 'Deployed to production', tone: 'success' },
  { time: '9:30', datetime: '2026-10-03T09:30', title: 'Checks failed on preview', tone: 'danger' }
]} />

Stat

A key figure with its change, read as "Up 4.2%". Invert it for figures where down is good.

Revenue

$48,210

Up 12.4% vs last month

Error rate

0.42%

Down 18% vs last week

Active users

1,204

Down 3.1%

Uptime

99.98%

No change 30 days

Code
import { Stat } from '$lib/ui';

<Stat label="Revenue" value="$48,210" delta={12.4} help="vs last month" series={revenue} />
<Stat label="Error rate" value="0.42%" delta={-18} invert />

AvatarGroup

Overlapping avatars with a "+N" for the rest, which names who it hides.

Code
import { AvatarGroup } from '$lib/ui';

<AvatarGroup label="Reviewers" max={3} people={[
  { name: 'Ada Lovelace' }, { name: 'Alan Turing' },
  { name: 'Grace Hopper' }, { name: 'Edsger Dijkstra' }
]} />

CodeBlock

Code with a file name, optional line numbers that never get copied, and a copy button that says it worked.

src/routes/users/+page.svelte svelte
<script lang="ts">
	import { DataTable } from '$lib/ui';
	export let data;
</script>

<DataTable caption="Users" rows={data.users} {columns} selectable />
sh
bun run deploy
Code
import { CodeBlock } from '$lib/ui';

<CodeBlock filename="src/routes/+page.svelte" language="svelte" lineNumbers code={source} />

Prose

Sets HTML you did not mark up yourself — Markdown, a CMS body — on the type scale.

Shipping a release

Every release goes through the same three steps, and none of them is skipped on a Friday.

  1. Tag the commit.
  2. Let CI build and test it.
  3. Promote the preview.
Small releases are boring releases. Boring is the goal.

Run bun run deploy once the checks are green.

StepOwner
TagRelease lead
PromoteOn-call

Questions go in #releases.

Code
import { Prose } from '$lib/ui';

<Prose>{@html post.html}</Prose>

Widgets

Stat

A number, its change, and a sparkline in the chart palette.

Signups

312

-4%
Code
{ type: 'stat', props: { label: 'Signups', value: '312', delta: -4, series: [42, 39, 44, 37] } }

Meter

Progress toward a goal, or usage against a limit that warns as it fills.

8.2 GB of 10 GB

Storage 82%
Code
{ type: 'meter', props: { label: 'Storage', value: 8.2, goal: 10, unit: ' GB', limit: true } }

Clock

Ticks every second through the board’s live channel, never into saved layout.

Saturday, Oct 3 · your time

Code
{ type: 'clock', props: { label: 'Lisbon', timeZone: 'Europe/Lisbon' } }

Checklist

A short to-do list with a done count.

2 of 3 done

Code
{ type: 'checklist', props: { items: [{ id: 'a', text: 'Write tests', done: true }] } }

Notes

A scratch pad.

Code
{ type: 'notes', props: { text: 'Remember the release notes.' } }

Widgets render inside the drag-and-drop <WidgetBoard>. Each is one manifest entry and one registry line in $lib/widgets; see the documentation for the board.