Module library

The blocks every app ends up building

72 modules in 6 groups. Each is built from the component kit, takes its data and copy as props, emits events, and never fetches. Mobile first at 320px, themed with CSS variables in light, dark and forced colors, and usable by keyboard and screen reader. Import from $lib/modules.

Every one is free and MIT-licensed. The demos below run on fake local data, so nothing you type leaves this page.

Account

SignInForm

Email and password sign-in with show/hide password, "keep me signed in", optional provider buttons (OAuth), and loading and error states.

Try the password wrong-password to see the error state.

Accessibility. A labelled form in a section named by its heading (level is a prop). Fields have real labels, autocomplete tokens (username, current-password) and aria-invalid + aria-describedby errors. A failed submit moves focus to an error summary whose links jump to each field; a server failure is announced as an alert. While loading the button sets aria-busy and ignores clicks without being disabled, so focus is not lost.

Code
import { SignInForm } from '$lib/modules';

<SignInForm
  providers={[{ id: 'github', label: 'Continue with GitHub' }]}
  loading={busy}
  error={failure}
  forgotHref="/reset"
  signUpHref="/sign-up"
  on:submit={(e) => signIn(e.detail)}
  on:provider={(e) => startOAuth(e.detail.id)}
/>

SignUpForm

Name, email and password with a live strength meter and requirement checklist, a terms checkbox, and field-level errors.

Use [email protected] to see a server field error.

Accessibility. Each field shows its own error, tied with aria-invalid and aria-describedby; the password field also points at the strength meter and checklist. Strength is a word, and every requirement says met or not met in text, so nothing rests on colour. Failed submits focus an error summary; the terms label is a slot so it can hold links.

Code
import { SignUpForm } from '$lib/modules';

<SignUpForm
  errors={serverErrors}
  loading={busy}
  signInHref="/sign-in"
  on:submit={(e) => register(e.detail)}
>
  <span slot="terms">I accept the <a href="/terms">terms</a></span>
</SignUpForm>

PasswordStrength

A strength meter and a checklist of password requirements, for any new-password field.

Password strength: Not entered yet

Your password needs:

  • At least 8 characters (not met)
  • A lowercase letter (not met)
  • An uppercase letter (not met)
  • A number (not met)
  • A symbol (not met)

Accessibility. The bar is decorative; the strength word is a status region announced when it changes, and each requirement carries "(met)" or "(not met)" for screen readers. Link a password input to it with aria-describedby. Honours forced-colors.

Code
import { PasswordStrength } from '$lib/modules';

<input type="password" bind:value={password} aria-describedby="pw-strength" />
<PasswordStrength id="pw-strength" {password} />

FormErrorSummary

A list of everything wrong in a form, each item a link to its field.

Please fix the following

Accessibility. A labelled section with a heading (level is a prop) that takes focus when you call focus() after a failed submit, so a screen reader reads it. Each link moves focus into its field. It renders nothing when there are no errors.

Code
import { FormErrorSummary } from '$lib/modules';

<FormErrorSummary
  bind:this={summary}
  {errors}
  fields={[{ name: 'email', label: 'Email', id: 'email' }]}
/>

PasswordResetFlow

Three steps: request an email, enter the code (one-time-code input), set a new password, with a stepper and a resend countdown.

Reset your password

  1. Email (Current step)
  2. Code (Not started)
  3. New password (Not started)

Reset your password

We will send a code to this address.

Any email works. In a real app you would hold the step until the server answers (autoAdvance=false).

Accessibility. A Stepper marks the current step with aria-current. Each step change moves focus to that step's heading. The code is one labelled group of cells with one-time-code autocomplete. Resend is disabled with the time left in its text, and a polite status says when it is ready again. Errors are summarised and tied to their fields.

Code
import { PasswordResetFlow } from '$lib/modules';

<PasswordResetFlow
  bind:step
  autoAdvance={false}
  loading={busy}
  error={failure}
  on:request={(e) => sendCode(e.detail.email)}
  on:verify={(e) => checkCode(e.detail)}
  on:reset={(e) => changePassword(e.detail)}
  on:done={() => goto('/sign-in')}
/>

ProfileForm

Avatar upload with preview, name, email and a bio with a character counter. Tracks what changed and offers Save and Discard.

Profile

Profile photo

PNG, JPEG, WebP or GIF, up to 2 MB.

24 / 160

A sentence or two about you.

No changes yet.

Accessibility. The photo picker is a real button that opens the file dialog; picking a bad file shows its reason in text. Save and Discard are disabled until something changes, and a polite status says "You have unsaved changes" in words. The bio counter is announced politely. Errors are summarised and tied to their fields.

Code
import { ProfileForm } from '$lib/modules';

<ProfileForm
  {initial}
  saving={busy}
  saved={ok}
  on:save={(e) => save(e.detail)}
  on:discard={() => (ok = false)}
/>

SessionsList

Signed-in devices with a "Current session" badge, last-active times, revoke one, and revoke all others behind a confirmation.

Active sessions

Devices where you are signed in.

  • Firefox on Linux Current session

    Phoenix, US Last active:

  • Safari on iPhone

    Portland, US Last active:

  • Chrome on Windows

    Boston, US Last active:

  • Edge on Windows

    Lewiston, US Last active:

Sign out of all other sessions?

This signs out 3 other sessions. You stay signed in here.

Accessibility. A list of sessions; the current one carries a text badge, not only a colour. Each Revoke button is named for its device ("Revoke session on Firefox on Linux"). Times are real <time> elements. "Sign out of all other sessions" opens an alert dialog with Cancel focused. After a revoke, a status says which device was signed out and focus returns to the heading.

Code
import { SessionsList } from '$lib/modules';

<SessionsList
  {sessions}
  now={Date.now()}
  revoking={pendingIds}
  on:revoke={(e) => revoke(e.detail.id)}
  on:revokeAll={(e) => revokeMany(e.detail.ids)}
/>

DangerZone

Delete the account behind a type-to-confirm step in an alert dialog.

Delete account

Permanently delete your account and everything in it. This cannot be undone.

Delete your account?

All your data will be erased for good. This cannot be undone.

Accessibility. The dialog is an alertdialog on the native <dialog>: focus is trapped, Cancel is focused first and Escape cancels. Pressing delete before the phrase matches keeps the dialog open, explains what to type in text and moves focus to the field. The button describes itself with the warning text.

Code
import { DangerZone } from '$lib/modules';

<DangerZone
  confirmPhrase={account.name}
  deleting={busy}
  on:delete={() => deleteAccount()}
/>

TwoFactorSetup

Authenticator setup: QR code slot, copyable secret, verify the code, then recovery codes to copy or download.

Set up two-factor authentication

  1. Scan the QR code

    Open your authenticator app and scan this code.

    QR code appears here

    Or enter the key by hand

    Setup key

    JBSW Y3DP EHPK 3PXP
  2. Enter the code

    Type the 6-digit code your app shows.

    Authentication code
  3. Save your recovery codes

    Your recovery codes appear after you verify the code.

Accessibility. An ordered list of three parts, each with a heading. The secret is selectable text with a Copy button, for people who cannot scan. The code is a labelled one-time-code group with a text error. Copy and download results are announced in a status region. Finish stays disabled until "I have saved my recovery codes" is ticked.

Code
import { TwoFactorSetup } from '$lib/modules';

<TwoFactorSetup
  secret={secret}
  recoveryCodes={codes}
  verified={ok}
  error={failure}
  on:verify={(e) => check(e.detail.code)}
  on:complete={() => close()}
>
  <img slot="qr" src={qrDataUrl} alt="QR code for your authenticator app" />
</TwoFactorSetup>

AccountSwitcher

A menu of accounts or organisations with the current one marked, and an optional "add account" row.

Current account: Ada Lovelace

Accessibility. The ARIA menu-button pattern with radio items. Enter, Space or arrows open it on the current account; arrows, Home and End move and skip disabled accounts; a letter jumps to the next match; Escape closes and returns focus to the button. The current account has aria-checked, a check mark and the word "current" for screen readers.

Code
import { AccountSwitcher } from '$lib/modules';

<AccountSwitcher
  {accounts}
  bind:current
  addLabel="Add account"
  on:select={(e) => switchTo(e.detail.id)}
  on:add={() => goto('/accounts/new')}
/>

Settings

AppearanceSettings

Theme (light, dark, system as radio cards), density, text size, reduce motion, and a live preview. It emits changes and stores nothing.

Appearance

Theme
Density
100%

Scales text in the preview and, once saved, across the app.

Turns off animations and transitions.

Preview

The quick brown fox

This is how text, buttons and spacing will look with your choices.

Animation: playing

Accessibility. The theme choice is a real radio group in a fieldset, so arrow keys move between cards and the selected card carries a check mark as well as a border. The slider and switch are native controls with labels and hints; the preview is a labelled group and its animation stops with reduce motion or the OS setting.

Code
import { AppearanceSettings } from '$lib/modules';

let appearance = { theme: 'system', density: 'comfortable', textSize: 100, reduceMotion: false };

<AppearanceSettings bind:value={appearance} on:change={(e) => save(e.detail)} />

NotificationPreferences

A matrix of events by channels with select-all for every row, every column and the whole grid, mixed states, and quiet hours per channel.

Notifications

Choose how you hear about each event
All
New comment Someone replies to you
Invoice paid A customer pays
Security alert A sign-in from a new device

4 / 9 notifications on

Quiet hours

Hold notifications on a channel during these hours.

Email

Quiet from 22:00 to 07:00 (9 h)

Push

Accessibility. A real table with row and column headers inside a keyboard-scrollable region. Every checkbox has a name that includes its event and channel; select-all boxes use the native indeterminate state for "some". Quiet hours validate with messages tied to the fields, and the on-count is a polite status.

Code
import { NotificationPreferences } from '$lib/modules';

<NotificationPreferences
  events={[{ id: 'comment', label: 'New comment' }]}
  channels={[{ id: 'email', label: 'Email', quietHours: true }, { id: 'push', label: 'Push' }]}
  bind:value
  bind:quietHours
  on:change={(e) => save(e.detail)}
/>

ApiKeysManager

Create keys with scopes and an expiry, show the secret once with a copy button, list keys masked with last use and expiry, and revoke with a confirmation.

API keys

  • CI deploy

    nk_live_••••••••a91f

    2 scopes

    Created
    Apr 2, 2026
    Last used
    17 hours ago
    Expires
    Expires soon: Jun 20, 2026
  • Reporting script

    nk_live_••••••••03cc

    Read only: 1 scope

    Created
    Jan 12, 2026
    Last used
    Never used
    Expires
    Never expires

Create an API key

Pick only the access this key needs.

Where the key is used, such as "CI deploy".

Scopes

Revoke this key?

Anything using it stops working straight away. This cannot be undone.

Accessibility. The create dialog is a native modal with a validation summary (role alert) and per-field errors. Focus moves to the one-time secret when it appears, back to the create button when it is dismissed, and to the heading after a revoke. Copy results are announced; revoking uses an alert dialog where Cancel has focus first.

Code
import { ApiKeysManager } from '$lib/modules';

<ApiKeysManager
  {keys}
  scopes={[{ value: 'users:read', label: 'Read users' }, { value: 'users:write', label: 'Write users' }]}
  {createdSecret}
  on:create={(e) => createKey(e.detail)}
  on:revoke={(e) => revokeKey(e.detail.id)}
  on:secretdismiss={() => (createdSecret = '')}
/>

WebhooksManager

Webhook endpoints with an add and edit dialog (URL validation, event multi-select), an enable switch, a health badge and recent deliveries.

Webhooks

  • https://example.com/hooks/billing

    Invoice paid

    Some failures
    Recent deliveries (3)
    • Delivered 200 Invoice paid
    • Failed 500 Invoice paid
    • Delivered 200 Invoice paid
  • https://ops.example.org/nebula

    Project created, User invited

    No data
    Recent deliveries (0)

    No deliveries yet.

Add a webhook endpoint

We send a signed POST request to this URL when the events you pick happen.

Must start with https://

Events

Delete this endpoint?

Deliveries stop straight away. This cannot be undone.

Accessibility. Switches, edit and delete buttons name their endpoint. The dialog shows a summary of errors as an alert and ties each message to its field. Delivery results are words ("Delivered 200", "Failed 500"), never colour alone, and the log is a disclosure list.

Code
import { WebhooksManager } from '$lib/modules';

<WebhooksManager
  {endpoints}
  events={[{ value: 'invoice.paid', label: 'Invoice paid' }]}
  on:save={(e) => save(e.detail)}
  on:toggle={(e) => setEnabled(e.detail)}
  on:remove={(e) => remove(e.detail.id)}
/>

PrivacyControls

Request an export of your data or its deletion, with each request moving through requested, in progress, ready, failed and cancelled.

Your data

Export my data

We will gather everything we hold about you into a file you can download.

Status: Not requested

Delete my data

Permanently erase your account and everything attached to it.

Status: Not requested

Delete all your data?

This cannot be undone. Export first if you want a copy.

The button turns on when the words match.

Accessibility. Status is text in a status region, not just a colour. Deletion needs a typed phrase in a dialog and the confirm button stays off until it matches. When the app answers a request the person made, focus moves to that status; updates that arrive on their own never steal focus.

Code
import { PrivacyControls } from '$lib/modules';

<PrivacyControls
  {exportRequest}
  {deleteRequest}
  on:exportrequest={startExport}
  on:deleterequest={startDelete}
  on:cancelrequest={(e) => cancel(e.detail.kind)}
/>

LanguageRegionSettings

Language, a searchable time zone combobox, date style, clock, number format and currency, with a live preview formatted by Intl.

Language and region

Type a city to search. New York.

Clock

Preview

Date
Jun 15, 2026
Time
8:00 AM
Number
1,234,567.891
Currency amount
$1,234.50
Percentage
26%

Accessibility. Every field is labelled; the time zone is an ARIA combobox with type-ahead on city names and a result count. The preview is a labelled group that updates politely, and dates are formatted for an explicit time zone so the output never depends on the viewer.

Code
import { LanguageRegionSettings } from '$lib/modules';

<LanguageRegionSettings bind:value={region} now={new Date().toISOString()} on:change={(e) => save(e.detail)} />

IntegrationsList

Cards for each service with a status badge, a permissions disclosure, and connect, reconnect or disconnect buttons. Disconnecting asks first.

Integrations

1 / 4 connected

  • Google Drive
    Needs attention

    Attach files from Drive.

    Permissions (2)
    • See file names
    • Download files you pick
  • Slack
    Connected

    Post project updates to a channel.

    Permissions (2)
    • Read channel names
    • Post messages as NebulaKit
  • GitHub
    Not connected

    Link repositories to projects.

    Permissions (2)
    • Read repository metadata
    • Receive push events
  • Stripe
    Not connected

    Show payments beside customers.

    Permissions (0)

    This integration asks for no access.

Disconnect this integration?

It loses access straight away. You can connect it again later.

Accessibility. Each card is a list item with a heading at the next level. Status is a word in a badge. Buttons carry the service name for screen readers, the permissions disclosure is a native details element, and disconnect uses an alert dialog with Cancel focused first.

Code
import { IntegrationsList } from '$lib/modules';

<IntegrationsList
  {integrations}
  busyId={pending}
  on:connect={(e) => connect(e.detail.id)}
  on:disconnect={(e) => disconnect(e.detail.id)}
/>

SettingsLayout

The shell of a settings page: a sticky side nav on wide screens, a select on narrow ones. One section shows at a time through the default slot.

Account settings

Profile

How you appear to others.

This is the profile panel. Put any module here, for example the notification matrix or the privacy controls.

Accessibility. The nav is a labelled landmark whose current link has aria-current="page"; arrow keys, Home and End move between links. Choosing a section moves focus to its panel, which is a named region. The hash deep-links to a section, and the mobile select is a native control.

Code
import { SettingsLayout } from '$lib/modules';

<SettingsLayout sections={[{ id: 'profile', label: 'Profile' }, { id: 'alerts', label: 'Notifications' }]} bind:active let:active>
  {#if active === 'profile'}<ProfileForm />{:else}<NotificationPreferences ... />{/if}
</SettingsLayout>

Marketing

Hero

A headline, lede and up to two calls to action with a media slot, centered or split beside the media.

New in v2

Build the product, not the plumbing

A starter kit with auth, billing and a component library already wired up.

See the demo

Primary clicked 0 times.

Accessibility. The headline is a heading at a configurable level and labels its section. CTAs with an href are links, otherwise buttons. Split layout stacks below 48rem so nothing scrolls sideways at 320px. Buttons are at least 44px tall.

Code
import { Hero } from '$lib/modules';

<Hero title="Ship faster" lede="A starter kit." layout="split" primary={{ label: 'Get started', href: '/signup' }}>
	<img slot="media" src="/shot.png" alt="The dashboard" />
</Hero>

FeatureGrid

Feature cards with an optional icon and link, one, two or three columns by width.

Why teams choose us

  • Fast by default

    Server-rendered at the edge, hydrated only where it matters.

    Learn more– Fast by default
  • Accessible

    Every control is labelled and keyboard-operable out of the box.

  • Themeable

    One set of tokens drives light, dark and high contrast.

    Learn more– Themeable

Accessibility. A list of cards under a configurable heading level. Icons are decorative (aria-hidden). Link text is extended with the feature title so "Learn more" is never ambiguous out of context.

Code
import { FeatureGrid } from '$lib/modules';

<FeatureGrid title="Why us" columns={3} features={[{ title: 'Fast', text: 'Edge rendered.', href: '/fast' }]}>
	<span slot="icon">*</span>
</FeatureGrid>

PricingTable

Plans with a monthly or annual switch, savings text, a highlighted plan and feature checklists, priced with Intl.

Simple pricing

Billing period

Save up to 17% with annual billing

  • Free

    For trying things out.

    Free

     

    • Included: 1 project
    • Included: Community support
    • Not included: Custom domain
  • Pro
    Most popular

    For growing teams.

    $24 / month

    Billed monthly

    • Included: Unlimited projects
    • Included: Priority support
    • Included: Custom domain
  • Team

    For whole companies.

    $79.50 / month

    Billed monthly

    • Included: Unlimited projects
    • Included: Priority support
    • Included: Single sign-on

Chosen: nothing yet

Accessibility. The billing switch is a labelled radio group, so arrow keys work natively. Every feature says Included or Not included in text for screen readers. The highlight is a border plus a badge, not colour alone. Plans stack on a phone. Fires select and intervalchange.

Code
import { PricingTable } from '$lib/modules';

<PricingTable plans={plans} bind:interval currency="EUR" locale="de-DE" on:select={(e) => choose(e.detail.planId)} />

ComparisonTable

Plans across, features down, as a real table that scrolls inside its own region.

Compare plans
Feature FreeProTeam
Projects 1 Unlimited Unlimited
Custom domain Not included Included Included
Priority support Not included Included Included
Single sign-on Not included Not included Included
Audit log Not included Not included Included

Accessibility. A true table with a caption, column and row headers. The scroll region is labelled and focusable so the keyboard can scroll it; the first column is sticky. Ticks and dashes carry Included / Not included text.

Code
import { ComparisonTable } from '$lib/modules';

<ComparisonTable caption="Plan comparison" plans={plans} rows={rows} highlight="pro" />

LogoCloud

A wrapping grid of customer logos. No marquee, nothing moves.

Trusted by teams at

  • Acme
  • Globex
  • Initech
  • Umbrella
  • Hooli
  • Stark

Accessibility. Every logo image has the company name as alt text; logos without an image fall back to text. Linked logos are real links. The heading can be visually hidden but stays in the outline.

Code
import { LogoCloud } from '$lib/modules';

<LogoCloud logos={[{ name: 'Acme', src: '/acme.svg', href: 'https://acme.test' }]} />

StatsStrip

Big numbers with labels that count up when scrolled into view.

Customers
12,400+
Uptime
99.98%
Countries
86
Rating
4.9 / 5

Accessibility. A description list. Counting is skipped under reduced motion. The moving number is aria-hidden and the final value is always available as text, so nothing is read half-counted.

Code
import { StatsStrip } from '$lib/modules';

<StatsStrip stats={[{ label: 'Customers', value: 12000, suffix: '+' }]} />

FaqSection

An accordion of questions with a search box that filters them.

Frequently asked questions

Accessibility. Each question is a heading holding a button with aria-expanded and aria-controls. A polite live region reports the match count or that nothing matches, and a visible empty message appears as well. Search matches ignore case and accents.

Code
import { FaqSection } from '$lib/modules';

<FaqSection items={faqs} single bind:query on:toggle={(e) => log(e.detail)} />

NewsletterSignup

Email, consent and a honeypot, with idle, submitting, success and error states.

Stay in the loop

One email a month. Unsubscribe whenever you like.

The first valid submit succeeds, the next one fails, and so on.

Accessibility. Invalid input shows errors tied to the field with aria-invalid and aria-describedby, and focus moves to the first. Outcomes are announced by a status live region and focus moves to it on success. The honeypot is hidden from people and assistive technology.

Code
import { NewsletterSignup } from '$lib/modules';

<NewsletterSignup status={status} message={note} on:subscribe={(e) => save(e.detail.email)} />

ContactForm

Name, email, topic, message with a live counter, a validation summary and a success state.

Contact us

0 / 500

The first valid send fails, the next succeeds.

Accessibility. A failed submit shows an alert summary with links to each field and moves focus to it; each field is tied to its error. The message has a polite character counter. Success replaces the form with a confirmation that takes focus.

Code
import { ContactForm } from '$lib/modules';

<ContactForm status={status} on:send={(e) => post(e.detail)} on:reset={() => (status = 'idle')} />

CtaBanner

A closing message with one or two actions.

Ready to build?

Start free. No credit card needed.

Accessibility. A labelled section with a configurable heading level. Actions are links when given an href, buttons otherwise, and are at least 44px tall.

Code
import { CtaBanner } from '$lib/modules';

<CtaBanner title="Ready?" text="Start free." primary={{ label: 'Sign up', href: '/signup' }} />

AnnouncementBar

A slim dismissible notice that remembers through a callback, not storage.

Dismissed 0 times. Nothing is stored.

Accessibility. A labelled aside, not an alert, so it never interrupts. The dismiss button has a name and a 44px target. Dismissing fires dismiss; the app decides whether to remember it and passes open={false} later.

Code
import { AnnouncementBar } from '$lib/modules';

<AnnouncementBar message="v2 is out." link={{ label: 'Read more', href: '/v2' }} on:dismiss={() => remember()} />

TeamGrid

People with a picture or initials, role, short bio and links.

Meet the team

  • Ada Lovelace

    Engineering

    Builds the engine.

  • Grace Hopper

    Product

  • Alan Turing

    Research

    Asks the hard questions.

Accessibility. A list of cards with a heading per person at a configurable level. Avatars fall back to initials and are named. Link names include the person ("Ada on GitHub").

Code
import { TeamGrid } from '$lib/modules';

<TeamGrid title="The team" members={[{ name: 'Ada Lovelace', role: 'Engineer', links: [{ label: 'GitHub', href: '#' }] }]} />

App shell

AppShell

The frame of an app: skip link, header, collapsible sidebar, main landmark and footer. Below its breakpoint the sidebar becomes an off-canvas panel.

Skip to main content
Acme

Overview

Content for Overview. Use the menu button to collapse the sidebar.

© Acme 2026

Accessibility. A skip link is the first tab stop and moves focus to <main>. The sidebar is a navigation landmark; off-canvas it is a modal <dialog>, so focus is trapped, Escape closes it and focus returns to the menu button. The toggle button states aria-expanded.

Code
import { AppShell } from '$lib/modules';

<AppShell bind:collapsed navLabel="Main navigation">
  <span slot="brand">Acme</span>
  <UserMenu slot="header" {user} />
  <ul slot="nav">…</ul>
  <h1>Dashboard</h1>
  <p slot="footer">© Acme</p>
</AppShell>

NotificationCenter

A bell with an unread badge opening a list of notifications grouped by day, with mark read, mark all read and an empty state.

With items Empty

Accessibility. The unread count is in the bell’s accessible name (“Notifications, 3 unread”), not only the badge. The list opens in a labelled dialog popover; arrow keys, Home and End move between rows; unread rows say “unread” in words as well as showing a dot. Escape closes and returns focus to the bell.

Code
import { NotificationCenter } from '$lib/modules';

<NotificationCenter
  bind:items
  now={new Date()}
  timeZone="America/Phoenix"
  on:select={(e) => open(e.detail.id)}
/>

UserMenu

The avatar button at the end of a header, opening who you are, a list of account links and a Sign out action.

Accessibility. The ARIA menu button pattern. The button is named “Account menu for <name>”. Arrows, Home and End move; Escape closes and returns focus; Tab closes. Sign out is set apart by a separator, a warning symbol and colour.

Code
import { UserMenu } from '$lib/modules';

<UserMenu
  user={{ name: 'Ada Lovelace', email: '[email protected]' }}
  items={[{ id: 'profile', label: 'Profile', href: '/profile' }]}
  on:signout={signOut}
/>

OnboardingChecklist

A “get started” checklist with a progress bar, expandable steps, dismiss, and a celebration when every step is done.

Get started

Setup progress

1 of 3 done

  1. Collaboration works better with two.

    Invite

Accessibility. Each step is a disclosure button with aria-expanded; done steps say “Done” in words. Progress is a labelled progressbar plus text. The celebration is announced through a polite live region and its animation is skipped for prefers-reduced-motion.

Code
import { OnboardingChecklist } from '$lib/modules';

<OnboardingChecklist
  bind:steps
  on:complete={() => confetti()}
  on:dismiss={() => hide()}
/>

Wizard

A multi-step form shell with a Stepper, per-step validation that gates Next, Back and Finish, and a summary of the answers on the last step.

  1. Account (Current step) Who is this for?
  2. Plan (Not started) Pick what fits.
  3. Review (Not started) One last look.

Account

Who is this for?

Accessibility. Focus moves to the new step’s heading on every change and a polite live region announces “Step 2 of 4”. A refused Next shows a role="alert" error summary whose links focus the fields, and marks the step in the Stepper in words. Enter submits the step.

Code
import { Wizard, runWizardRules, wizardRequiredRule } from '$lib/modules';

<Wizard {steps} bind:values {validate} {summary} on:finish={save} let:step let:errors let:fieldId>
  {#if step.id === 'account'}
    <TextInput id={fieldId('name')} label="Name" bind:value={values.name} error={errors.name} />
  {/if}
</Wizard>

ErrorState

Full-section error pages for 404, 403, 500, offline and maintenance, with retry and home actions and a copyable request id.

500

Something went wrong

We hit an error on our side. Try again in a moment.

Go home
Request ID req_7f3a91c2

Accessibility. A section labelled by its heading (level configurable). Emoji icons are hidden from assistive tech; the message carries the meaning. Copy confirms through a polite live region. Set announce for an error that appears in place, which makes it role="alert".

Code
import { ErrorState } from '$lib/modules';

<ErrorState kind="500" requestId={id} on:retry={reload} />

EmptyStateCard

Empty states for “no data”, “no results” and “first run”, each with its own wording and a call to action.

No results

Nothing matches “invoice 2019”. Try a different search or clear the filters.

Accessibility. A section labelled by a real heading. The “no results” preset quotes the search back in words. Actions are links or buttons with 44px targets.

Code
import { EmptyStateCard } from '$lib/modules';

<EmptyStateCard kind="no-results" query={q} action={{ id: 'clear', label: 'Clear filters' }} />

ConfirmDialog

A promise-style confirmation for destructive actions: await dialog.ask() and get true or false.

Are you sure?

Accessibility. An alertdialog on the native <dialog>: Cancel takes focus first so a stray Enter never confirms, Escape cancels, the page behind is inert, and focus returns to the control that asked.

Code
import { ConfirmDialog } from '$lib/modules';

let dialog;
<ConfirmDialog bind:this={dialog} />
<button on:click={async () => { if (await dialog.ask({ title: 'Delete project?', confirmLabel: 'Delete' })) remove(); }}>Delete</button>

TabsPageNav

Tab-style navigation between pages (routes). The row scrolls inside itself when links do not fit, with an arrow at each edge that has more.

Accessibility. A labelled <nav> of real links; the current page has aria-current="page" and a bar under it. Badges join the link’s accessible name. The scroll region keeps natural Tab order, so every link stays reachable by keyboard.

Code
import { TabsPageNav } from '$lib/modules';

<TabsPageNav {items} current={$page.url.pathname} />

Toolbar

A formatting-style toolbar with groups, toggle buttons and a “More” menu that takes the items that do not fit.

Accessibility. The ARIA toolbar pattern with a roving tabindex: one tab stop, arrows, Home and End move. Toggles use aria-pressed. The overflow button is the last stop and opens a menu with arrow-key navigation and Escape to return.

Code
import { Toolbar } from '$lib/modules';

<Toolbar label="Text formatting" bind:groups on:action={(e) => apply(e.detail)} />

BottomNav

The mobile bottom tab bar: icons, labels and badges, safe-area aware, hidden once a sidebar fits.

On a phone this sits at the bottom of the screen and hides on wider ones. It is kept visible here so you can see it.

Accessibility. A labelled <nav> of links with aria-current="page"; the current one is bold with a bar, not colour alone. Badges are spoken (“Inbox, 3 new”). Touch targets are at least 44px, and padding honours the device’s safe-area insets.

Code
import { BottomNav } from '$lib/modules';

<BottomNav {items} current={$page.url.pathname} fixed />

Data & teams

DataGrid

A data table that searches, sorts on several columns, filters by column, pages, selects rows with a bulk-action bar, offers a row menu and a density toggle, and exports the current view as CSV.

Filters

37 results

People
Actions
Ada Lovelace Engineer Active 12 2015-01-10
Alan Turing Support Offline 209 2016-02-11
Grace Hopper Manager Away 406 2017-03-12
Barbara Liskov Designer Active 603 2018-04-13
Edsger Dijkstra Engineer Offline 800 2019-05-14
Margaret Hamilton Support Away 997 2020-06-15
Donald Knuth Manager Active 1,194 2021-07-16
Radia Perlman Designer Offline 1,391 2022-08-17

1–8 of 37

Accessibility. A labelled region around a real table: sortable headers are buttons with aria-sort (Shift adds a column), the select-all checkbox goes indeterminate, every row checkbox and action button names its row, result counts and sort changes are announced, loading is aria-busy, errors offer a retry. Below about 40rem of its own width the rows become cards whose cells carry their column label.

Code
import { DataGrid } from '$lib/modules';

<DataGrid
  label="People"
  {rows}
  columns={[
    { key: 'name', label: 'Name', sortable: true },
    { key: 'team', label: 'Team', sortable: true, filter: 'select', options }
  ]}
  selectable
  bind:selected
  rowActions={[{ id: 'edit', label: 'Edit' }]}
  bulkActions={[{ id: 'delete', label: 'Delete', danger: true }]}
  on:bulkaction={onBulk}
  on:export={(e) => download(e.detail.csv, e.detail.name)}
/>

FilterBar

Active filters as removable chips, an Add filter menu for the ones not yet in use, Clear all, and a spoken result count.

  • Status: Paid

3 results

  • #1 Ada — paid, EU
  • #3 Grace — paid, US
  • #5 Edsger — paid, APAC

Accessibility. A labelled group: each chip has a remove button that names its filter, adding a filter opens a labelled form (Escape cancels) and returns focus to the menu, and every add, remove or clear plus the new result count is announced in a status region.

Code
import { FilterBar, toColumnFilters } from '$lib/modules';

<FilterBar
  {defs}
  bind:active
  count={visibleRows.length}
/>
<DataGrid {rows} {columns} filters={toColumnFilters(defs, active)} label="Orders" />

KanbanBoard

Columns of cards you can drag with a pointer or finger, move with the keyboard or a Move menu, limit with WIP caps and add to inline.

To do

Limit: 3
  • Write the launch post
    • content
  • Fix the empty-cart bug
    • bug
  • Audit colour contrast

In progress

Limit: 2 / 3
  • Billing webhooks

    Retries and idempotency.

  • Onboarding emails

Done

Limit: 1
  • Set up CI

Accessibility. Every column is a labelled section with a configurable heading level. Cards move by drag, by Space then arrows on the grip, by Alt+arrows, or from a menu that names each destination. Every move or refusal is announced in a live region; a full column says "At limit" in text, not just colour.

Code
import { KanbanBoard } from '$lib/modules';

<KanbanBoard
  label="Sprint board"
  columns={[{ id: 'todo', title: 'To do' }, { id: 'doing', title: 'Doing', limit: 3 }]}
  bind:cards
  on:move={(e) => save(e.detail)}
/>

ActivityFeed

A feed grouped by day with avatars, relative times that carry the exact time, and a Load more button.

Today

  1. Ada Lovelace commented on Billing webhooks

  2. Alan Turing merged Pull request #212

    Retry failed webhook deliveries with backoff.

  3. Grace Hopper invited [email protected]

Yesterday

  1. Barbara Liskov closed Issue #98

Accessibility. Each day is a heading over an ordered list. Times are <time> elements whose datetime and title are exact. Today and Yesterday come from the now and timeZone you pass, so server and browser agree. After Load more, focus moves to the first new item.

Code
import { ActivityFeed } from '$lib/modules';

<ActivityFeed
  label="Recent activity"
  {items}
  now={new Date('2026-10-13T15:00:00Z')}
  hasMore
  on:loadmore={loadOlder}
/>

CommentThread

A discussion with replies nested three levels deep, a compose box with a character limit, edit and delete on your own comments, emoji reactions and collapsible branches.

Comments (5)

  • Alan Turing

    Should the retry window be configurable?

    • Ada Lovelace

      Yes, per endpoint. Default to 24 hours.

      • Alan Turing

        Agreed. I will add it to the schema.

      • Ada Lovelace

        Thanks! This one is nested at the deepest level, so it cannot be replied to.

  • Grace Hopper

    Docs updated.

Delete this comment?

This cannot be undone.

Accessibility. The thread is a nested list of articles. The compose field is labelled and tied to its character count and error (aria-describedby, aria-invalid, role="alert"); the remaining count is only spoken near the limit. Reactions are toggle buttons with aria-pressed, branches use aria-expanded, delete asks first, and focus lands on the new or edited comment.

Code
import { CommentThread } from '$lib/modules';

<CommentThread
  bind:comments
  currentUser={{ id: 'u1', name: 'Ada Lovelace' }}
  {now}
  maxLength={500}
  on:post={(e) => save(e.detail.comment)}
/>

TeamMembers

Members with role selects and confirmed removal, an invite dialog that takes many email addresses at once, and pending invites you can resend or revoke.

Members (4)

  • Ada Lovelace you
  • Alan Turing
  • Grace Hopper
  • Edsger Dijkstra

The last owner cannot be changed or removed. Make someone else an owner first.

Pending invites (2)

Invite people

Type or paste email addresses. Press Enter or comma after each.

Remove this member?

Revoke this invite?

Accessibility. Role selects and buttons carry the member name. The last owner is locked with the reason available to assistive technology. The invite dialog uses a tag input that refuses invalid, duplicate and existing addresses with a message. Removing and revoking ask first, and results are announced.

Code
import { TeamMembers } from '$lib/modules';

<TeamMembers
  bind:members
  bind:invites
  currentUserId="u1"
  {now}
  on:invite={(e) => api.invite(e.detail.emails, e.detail.role)}
/>

FileManager

Browse folders, switch between list and grid, upload with a progress queue you can cancel, rename in place, select several files and delete with confirmation.

All files (4)

  • Folder · 2 items
  • Folder · 2 items
  • Archive · 52 MB · Jul 1, 2026
  • Text · 900 B · Oct 12, 2026

Delete permanently?

0 items. This cannot be undone.

Accessibility. Breadcrumbs are a labelled nav with aria-current. Items have a checkbox, a menu and a name button, and each file type shows an icon plus a word. Rename is an inline labelled field (Enter saves, Escape cancels) with errors tied to it. Uploads are a list of real progress bars with a named Cancel button; drops, navigation and deletes are announced.

Code
import { FileManager, setUploadProgress } from '$lib/modules';

<FileManager
  label="Project files"
  bind:entries
  bind:folderId
  bind:uploads
  maxBytes={5_000_000}
  on:upload={(e) => start(e.detail.jobs, e.detail.files)}
/>

CalendarSchedule

A month grid with events, previous, next and Today controls, the agenda for the picked day, and a dialog to add, edit and delete events.

October 2026

MonTueWedThuFriSatSun

Agenda: Tuesday, October 13, 2026

Add event

Accessibility. The month is an ARIA grid with one tab stop: arrows move by day or week, Home and End to the ends of the week, PageUp and PageDown by month. Each day is named with its date and event count, today is marked with aria-current, the month title is a polite live region, and the agenda list carries the same events in plain text for narrow screens.

Code
import { CalendarSchedule } from '$lib/modules';

<CalendarSchedule
  label="Team calendar"
  bind:events
  {now}
  weekStart={1}
  on:add={(e) => api.create(e.detail.event)}
/>

TreeView

A hierarchy with expand and collapse, selection and type-ahead, built to the WAI-ARIA tree pattern.

  • src
  • lib
  • modules
  • app.css
  • package.json
  • README.md

Selected: button

Accessibility. role="tree" with treeitems exposing aria-level, aria-posinset, aria-setsize, aria-expanded and aria-selected. One item is in the tab order; Up and Down move, Right opens then enters, Left closes then goes to the parent, Home and End jump, * opens siblings, Enter or Space selects, and typing letters jumps to the next matching label.

Code
import { TreeView } from '$lib/modules';

<TreeView
  label="Project files"
  {nodes}
  bind:expanded
  bind:selected
  on:select={(e) => open(e.detail.id)}
/>

AuditLog

A filterable table of events with severity badges, expandable JSON details and CSV export.

12 events

Security events
Severity Actor Action Target Details
Critical ada api_key.revoke key_live_41
Warning alan member.role_change grace
Notice grace export.create customers.csv
Info ada user.login —
Warning system login.failed edsger
Info edsger user.login —

Accessibility. Filters sit in a labelled search form with a real fieldset for severities; the matching count is announced. Severity is always a word beside the badge colour. Each row has a details button with aria-expanded and aria-controls naming its event, and the table scrolls inside a focusable labelled region on narrow screens.

Code
import { AuditLog } from '$lib/modules';

<AuditLog
  label="Security events"
  {events}
  timeZone="UTC"
  on:export={(e) => download(e.detail.csv, e.detail.name)}
/>

Commerce

BillingOverview

The current plan, its price, the renewal date and usage meters, with upgrade and cancel actions. Cancelling asks first.

Billing

Active
Current plan
Pro
Price
$29.00 per month
Renews on
(in 19 days)

Usage this period

Resets Nov 1, 2026 (in 19 days)

  • API calls Near limit

    8,400 of 10,000 calls

  • Storage Within limit

    3.2 of 50 GB

  • Seats Over by 2 seats

    12 of 10 seats

  • Emails sent Unlimited

    4,200 used

Cancel your plan?

You keep access until November 1, 2026. After that your account moves to the free plan.

Accessibility. A labelled section with a definition list; the status is a badge with words; renewal is a <time>. Past-due and ending-soon notices are alerts. Cancel opens an alertdialog that focuses "Keep plan" first.

Code
import { BillingOverview } from '$lib/modules';

<BillingOverview
  plan={{ name: 'Pro', priceCents: 2900, interval: 'month' }}
  renewsAt="2026-11-01T00:00:00Z"
  meters={[{ id: 'api', label: 'API calls', used: 8400, limit: 10000 }]}
  on:upgrade={openPlans}
  on:cancel={cancelPlan}
/>

UsageMeters

Quota bars with limits, a near-limit warning, an overage state and the reset date. A null limit means unlimited.

Usage this period

Resets Nov 1, 2026 (in 19 days)

  • API calls Near limit

    8,400 of 10,000 calls

  • Storage Within limit

    3.2 of 50 GB

  • Seats Over by 2 seats

    12 of 10 seats

  • Emails sent Unlimited

    4,200 used

Accessibility. Each bar is role="meter" with min, max, now and a text value ("8,400 of 10,000 API calls. Near limit"). State is written as text with an icon, and an overage is also striped, never colour alone.

Code
import { UsageMeters } from '$lib/modules';

<UsageMeters
  meters={[{ id: 'seats', label: 'Seats', used: 12, limit: 10, unit: 'seats' }]}
  warnAt={0.8}
  resetsAt="2026-11-01T00:00:00Z"
  now={new Date()}
/>

InvoiceList

Billing history as a table that becomes cards on a phone, with status badges, download actions, totals and a pagination hook.

Invoices

Invoice Date Description Amount Status Download
INV-1012 Pro plan, monthly $29.00 Open
INV-1011 Pro plan, monthly $29.00 Overdue Download
INV-1010 Pro plan, monthly $29.00 Paid
INV-1009 Pro plan, monthly $29.00 Paid Download
Total paid
$232.00
Outstanding
$58.00

Page 1 of 3

Accessibility. Keeps table, row and cell roles in the stacked layout so a screen reader still gets a table. Status badges include the word. Each download has its own label ("Download invoice INV-1004"). Paging is announced.

Code
import { InvoiceList } from '$lib/modules';

<InvoiceList
  {invoices}
  pageSize={5}
  on:download={(e) => save(e.detail.invoice)}
  on:pagechange={(e) => load(e.detail.page)}
/>

PaymentMethodCard

A saved card: brand, masked last four, expiry with expired and expiring-soon states, set as default and remove.

Visa ending in 4242

Expires 12/28 Default

Cardholder: Ada Lovelace

Remove this card?

Visa ending in 4242 will be removed from your account.

American Express ending in 0005

Expires 11/26 Expires soon

Remove this card?

American Express ending in 0005 will be removed from your account.

Mastercard ending in 4444

Expires 03/25 Expired

Remove this card?

Mastercard ending in 4444 will be removed from your account.

Accessibility. An article named "Visa ending in 4242". Expired and expiring-soon are badges with words. Buttons name the card they act on. Remove asks in an alertdialog that focuses the safe choice.

Code
import { PaymentMethodCard } from '$lib/modules';

<PaymentMethodCard
  method={{ id: 'pm_1', brand: 'visa', last4: '4242', expMonth: 12, expYear: 2027, isDefault: true }}
  now={new Date()}
  on:setDefault={makeDefault}
  on:remove={removeCard}
/>

PaymentMethodForm

Add a card: number grouped by brand with a Luhn check, MM/YY expiry, a CVC whose length follows the brand. Demo only: plain inputs, no payment SDK.

Add a card

Demo fields. These are plain inputs: in production use your payment processor’s hosted fields so card numbers never reach your server.

3 digits

Accessibility. cc-name, cc-number, cc-exp and cc-csc autocomplete; numeric keyboards. Errors show inline after blur or submit, tied with aria-describedby and aria-invalid, and are summarised in a role="alert" box that takes focus with links to each field.

Code
import { PaymentMethodForm } from '$lib/modules';

<!-- Demo fields. In production use your processor's hosted fields. -->
<PaymentMethodForm on:submit={(e) => console.log(e.detail.brand, e.detail.last4)} />

CheckoutForm

Contact, a country-aware address, shipping method cards and an order summary. The summary is a disclosure on a phone and a sidebar from 48rem.

Checkout

Contact
Shipping address

e.g. 04240

Shipping method

Accessibility. Fieldsets with legends; shipping-* autocomplete; the region field is a select or text and the postal label follows the country. Submit errors are summarised in a focused role="alert" with links. The pay button keeps focus while loading; the total is a polite live region.

Code
import { CheckoutForm } from '$lib/modules';

<CheckoutForm
  {items}
  shippingMethods={methods}
  taxRate={0.08}
  loading={paying}
  on:submit={(e) => pay(e.detail)}
>
  <PaymentFields slot="payment" />
</CheckoutForm>

CartSummary

Line items with a quantity stepper and remove, a promo code box, and subtotal, discount, shipping, tax and total. Has an empty state.

Your cart

3 items

  • Ceramic mug

    White

    $18.00

    $36.00

  • Canvas tote

    Natural

    $24.00

    $24.00

Subtotal
$60.00
Shipping
$5.00
Tax
$4.80
Total
$69.80

Try the codes SAVE10 and SHIPFREE.

Accessibility. Steppers are labelled with the item name. Changes are announced in a polite status line, and focus returns to the heading after a removal. Promo errors are tied to the input. Totals are a polite live region.

Code
import { CartSummary } from '$lib/modules';

<CartSummary
  bind:items
  promos={[{ code: 'SAVE10', kind: 'percent', value: 10 }]}
  taxRate={0.08}
  on:checkout={goToCheckout}
/>

OrderStatusTracker

The steps an order goes through with where it is now and the estimated delivery. Vertical on a phone, horizontal on wide screens.

Order status

Order #10482

Estimated delivery:

  1. Order placed Completed

  2. Packed Completed

    Packed at the Lewiston warehouse

  3. Shipped Current step

    On its way with the courier

  4. Delivered Upcoming

Accessibility. An ordered list; the current step has aria-current="step". Each step says Completed, Current step or Upcoming in text, so state is not only a colour or an icon. Dates are <time> elements.

Code
import { OrderStatusTracker } from '$lib/modules';

<OrderStatusTracker
  steps={[{ id: 'placed', label: 'Order placed' }, { id: 'shipped', label: 'Shipped' }]}
  current={1}
  eta="2026-10-20"
/>

ProductCard

A product tile: image with alt text, price or sale price, rating, add to cart and a sold-out state.

White ceramic mug

Ceramic mug

4.5 out of 5 stars, 32 reviews

$18.00

Natural canvas tote bag Sale

Canvas tote

4 out of 5 stars, 8 reviews

Sale price: $21.00 Original price: $28.00

Sold out

Enamel pin

5 out of 5 stars, 1 review

$9.00

Accessibility. An article with a heading of your level. The image needs real alt text. Sale and original prices are announced ("Sale price", "Original price"). Sold out is a badge and a disabled button saying so. Add is confirmed in a polite status.

Code
import { ProductCard } from '$lib/modules';

<ProductCard
  product={{ id: 'p1', name: 'Mug', image: '/mug.jpg', imageAlt: 'White ceramic mug', priceCents: 1800, salePriceCents: 1400, rating: 4.5, reviewCount: 32 }}
  on:add={(e) => cart.add(e.detail.id)}
/>

ReviewsRatings

The average, a bar per star count, a sortable review list with helpful buttons, and an optional form to write a review.

Customer reviews

4.0

4 out of 5 stars, 4 reviews

Based on 4 reviews

  • 5 stars 50% (2)
  • 4 stars 25% (1)
  • 3 stars 0% (0)
  • 2 stars 25% (1)
  • 1 star 0% (0)

  • 5 out of 5 stars

    Perfect

    Ada ·

    Keeps my tea hot for ages and looks great.

  • 4 out of 5 stars

    Grace ·

    Lovely, a little smaller than I expected.

  • 2 out of 5 stars

    Chipped

    Linus ·

    Arrived with a chip on the rim. Support was quick to replace it.

  • 5 out of 5 stars

    Margaret ·

    Bought four as gifts.

Write a review

Your rating

Accessibility. Bars carry their numbers as text. The sort is a native select and the result is announced. Helpful is a toggle with aria-pressed. The write form uses a star-rating radio group and tied inline errors.

Code
import { ReviewsRatings } from '$lib/modules';

<ReviewsRatings {reviews} canWrite on:submit={postReview} on:helpful={markHelpful} />

RatingStars

Stars to read (with half stars) or to give (a radio group).

4.5 out of 5 stars, 12 reviews 3 out of 5 stars
Your rating

You chose 3 of 5.

Accessibility. Display: the stars are hidden and one text alternative ("4.5 out of 5 stars, 12 reviews") is read. Input: a fieldset with legend and real radios, so arrow keys and forms work; the filled state has an outline, not only a colour.

Code
import { RatingStars } from '$lib/modules';

<RatingStars value={4.5} count={12} showValue />
<RatingStars variant="input" label="Your rating" bind:value={mine} />

StatusPage

System components with status badges, 90-day uptime strips with a text alternative for every day, and an incident list.

System status

Updated October 13, 2026

  • Website

    Operational
    • Jul 16, 2026: No incidents
    • Jul 17, 2026: No incidents
    • Jul 18, 2026: No incidents
    • Jul 19, 2026: No incidents
    • Jul 20, 2026: No incidents
    • Jul 21, 2026: No incidents
    • Jul 22, 2026: No incidents
    • Jul 23, 2026: No incidents
    • Jul 24, 2026: No incidents
    • Jul 25, 2026: No incidents
    • Jul 26, 2026: No incidents
    • Jul 27, 2026: No incidents
    • Jul 28, 2026: No incidents
    • Jul 29, 2026: No incidents
    • Jul 30, 2026: No incidents
    • Jul 31, 2026: No incidents
    • Aug 1, 2026: No incidents
    • Aug 2, 2026: No incidents
    • Aug 3, 2026: No incidents
    • Aug 4, 2026: No incidents
    • Aug 5, 2026: Degraded performance, 14 minutes
    • Aug 6, 2026: No incidents
    • Aug 7, 2026: No incidents
    • Aug 8, 2026: No incidents
    • Aug 9, 2026: No incidents
    • Aug 10, 2026: No incidents
    • Aug 11, 2026: No incidents
    • Aug 12, 2026: No incidents
    • Aug 13, 2026: No incidents
    • Aug 14, 2026: No incidents
    • Aug 15, 2026: No incidents
    • Aug 16, 2026: No incidents
    • Aug 17, 2026: No incidents
    • Aug 18, 2026: No incidents
    • Aug 19, 2026: No incidents
    • Aug 20, 2026: No incidents
    • Aug 21, 2026: No incidents
    • Aug 22, 2026: No incidents
    • Aug 23, 2026: No incidents
    • Aug 24, 2026: No incidents
    • Aug 25, 2026: No incidents
    • Aug 26, 2026: No incidents
    • Aug 27, 2026: No incidents
    • Aug 28, 2026: No incidents
    • Aug 29, 2026: No incidents
    • Aug 30, 2026: No incidents
    • Aug 31, 2026: No incidents
    • Sep 1, 2026: No incidents
    • Sep 2, 2026: No incidents
    • Sep 3, 2026: No incidents
    • Sep 4, 2026: No incidents
    • Sep 5, 2026: No incidents
    • Sep 6, 2026: No incidents
    • Sep 7, 2026: No incidents
    • Sep 8, 2026: No incidents
    • Sep 9, 2026: No incidents
    • Sep 10, 2026: No incidents
    • Sep 11, 2026: No incidents
    • Sep 12, 2026: No incidents
    • Sep 13, 2026: No incidents
    • Sep 14, 2026: No incidents
    • Sep 15, 2026: Outage, 42 minutes
    • Sep 16, 2026: No incidents
    • Sep 17, 2026: No incidents
    • Sep 18, 2026: No incidents
    • Sep 19, 2026: No incidents
    • Sep 20, 2026: No incidents
    • Sep 21, 2026: No incidents
    • Sep 22, 2026: No incidents
    • Sep 23, 2026: No incidents
    • Sep 24, 2026: No incidents
    • Sep 25, 2026: No incidents
    • Sep 26, 2026: No incidents
    • Sep 27, 2026: No incidents
    • Sep 28, 2026: No incidents
    • Sep 29, 2026: No incidents
    • Sep 30, 2026: No incidents
    • Oct 1, 2026: No incidents
    • Oct 2, 2026: No incidents
    • Oct 3, 2026: No incidents
    • Oct 4, 2026: No incidents
    • Oct 5, 2026: No incidents
    • Oct 6, 2026: No incidents
    • Oct 7, 2026: No incidents
    • Oct 8, 2026: No incidents
    • Oct 9, 2026: No incidents
    • Oct 10, 2026: No incidents
    • Oct 11, 2026: No incidents
    • Oct 12, 2026: No incidents
    • Oct 13, 2026: No incidents
    98.33% uptime
    Days with issues
    • Aug 5, 2026: Degraded performance, 14 minutes
    • Sep 15, 2026: Outage, 42 minutes
  • API

    Degraded performance

    Elevated latency on some requests.

    • Jul 16, 2026: No incidents
    • Jul 17, 2026: No incidents
    • Jul 18, 2026: No incidents
    • Jul 19, 2026: No incidents
    • Jul 20, 2026: No incidents
    • Jul 21, 2026: No incidents
    • Jul 22, 2026: No incidents
    • Jul 23, 2026: No incidents
    • Jul 24, 2026: No incidents
    • Jul 25, 2026: No incidents
    • Jul 26, 2026: No incidents
    • Jul 27, 2026: No incidents
    • Jul 28, 2026: No incidents
    • Jul 29, 2026: No incidents
    • Jul 30, 2026: No incidents
    • Jul 31, 2026: No incidents
    • Aug 1, 2026: No incidents
    • Aug 2, 2026: No incidents
    • Aug 3, 2026: No incidents
    • Aug 4, 2026: No incidents
    • Aug 5, 2026: No incidents
    • Aug 6, 2026: No incidents
    • Aug 7, 2026: No incidents
    • Aug 8, 2026: No incidents
    • Aug 9, 2026: No incidents
    • Aug 10, 2026: No incidents
    • Aug 11, 2026: No incidents
    • Aug 12, 2026: No incidents
    • Aug 13, 2026: No incidents
    • Aug 14, 2026: No incidents
    • Aug 15, 2026: No incidents
    • Aug 16, 2026: No incidents
    • Aug 17, 2026: No incidents
    • Aug 18, 2026: No incidents
    • Aug 19, 2026: No incidents
    • Aug 20, 2026: No incidents
    • Aug 21, 2026: No incidents
    • Aug 22, 2026: No incidents
    • Aug 23, 2026: No incidents
    • Aug 24, 2026: No incidents
    • Aug 25, 2026: Outage, 8 minutes
    • Aug 26, 2026: No incidents
    • Aug 27, 2026: No incidents
    • Aug 28, 2026: No incidents
    • Aug 29, 2026: No incidents
    • Aug 30, 2026: No incidents
    • Aug 31, 2026: No incidents
    • Sep 1, 2026: No incidents
    • Sep 2, 2026: No incidents
    • Sep 3, 2026: No incidents
    • Sep 4, 2026: No incidents
    • Sep 5, 2026: No incidents
    • Sep 6, 2026: No incidents
    • Sep 7, 2026: No incidents
    • Sep 8, 2026: No incidents
    • Sep 9, 2026: No incidents
    • Sep 10, 2026: No incidents
    • Sep 11, 2026: No incidents
    • Sep 12, 2026: No incidents
    • Sep 13, 2026: No incidents
    • Sep 14, 2026: No incidents
    • Sep 15, 2026: No incidents
    • Sep 16, 2026: No incidents
    • Sep 17, 2026: No incidents
    • Sep 18, 2026: No incidents
    • Sep 19, 2026: No incidents
    • Sep 20, 2026: No incidents
    • Sep 21, 2026: No incidents
    • Sep 22, 2026: No incidents
    • Sep 23, 2026: No incidents
    • Sep 24, 2026: No incidents
    • Sep 25, 2026: No incidents
    • Sep 26, 2026: No incidents
    • Sep 27, 2026: No incidents
    • Sep 28, 2026: No incidents
    • Sep 29, 2026: No incidents
    • Sep 30, 2026: No incidents
    • Oct 1, 2026: No incidents
    • Oct 2, 2026: No incidents
    • Oct 3, 2026: No incidents
    • Oct 4, 2026: No incidents
    • Oct 5, 2026: No incidents
    • Oct 6, 2026: No incidents
    • Oct 7, 2026: No incidents
    • Oct 8, 2026: No incidents
    • Oct 9, 2026: No incidents
    • Oct 10, 2026: No incidents
    • Oct 11, 2026: No incidents
    • Oct 12, 2026: Degraded performance, 30 minutes
    • Oct 13, 2026: Degraded performance, 55 minutes
    97.78% uptime
    Days with issues
    • Aug 25, 2026: Outage, 8 minutes
    • Oct 12, 2026: Degraded performance, 30 minutes
    • Oct 13, 2026: Degraded performance, 55 minutes
  • Email delivery

    Operational
    • Jul 16, 2026: No incidents
    • Jul 17, 2026: No incidents
    • Jul 18, 2026: No incidents
    • Jul 19, 2026: No incidents
    • Jul 20, 2026: No incidents
    • Jul 21, 2026: No incidents
    • Jul 22, 2026: No incidents
    • Jul 23, 2026: No incidents
    • Jul 24, 2026: No incidents
    • Jul 25, 2026: No incidents
    • Jul 26, 2026: No incidents
    • Jul 27, 2026: No incidents
    • Jul 28, 2026: No incidents
    • Jul 29, 2026: No incidents
    • Jul 30, 2026: No incidents
    • Jul 31, 2026: No incidents
    • Aug 1, 2026: No incidents
    • Aug 2, 2026: No incidents
    • Aug 3, 2026: No incidents
    • Aug 4, 2026: No incidents
    • Aug 5, 2026: No incidents
    • Aug 6, 2026: No incidents
    • Aug 7, 2026: No incidents
    • Aug 8, 2026: No incidents
    • Aug 9, 2026: No incidents
    • Aug 10, 2026: No incidents
    • Aug 11, 2026: No incidents
    • Aug 12, 2026: No incidents
    • Aug 13, 2026: No incidents
    • Aug 14, 2026: No incidents
    • Aug 15, 2026: No incidents
    • Aug 16, 2026: No incidents
    • Aug 17, 2026: No incidents
    • Aug 18, 2026: No incidents
    • Aug 19, 2026: No incidents
    • Aug 20, 2026: No incidents
    • Aug 21, 2026: No incidents
    • Aug 22, 2026: No incidents
    • Aug 23, 2026: No incidents
    • Aug 24, 2026: No incidents
    • Aug 25, 2026: No incidents
    • Aug 26, 2026: No incidents
    • Aug 27, 2026: No incidents
    • Aug 28, 2026: No incidents
    • Aug 29, 2026: No incidents
    • Aug 30, 2026: No incidents
    • Aug 31, 2026: No incidents
    • Sep 1, 2026: No incidents
    • Sep 2, 2026: No incidents
    • Sep 3, 2026: No incidents
    • Sep 4, 2026: No incidents
    • Sep 5, 2026: No incidents
    • Sep 6, 2026: No incidents
    • Sep 7, 2026: No incidents
    • Sep 8, 2026: No incidents
    • Sep 9, 2026: No incidents
    • Sep 10, 2026: No incidents
    • Sep 11, 2026: No incidents
    • Sep 12, 2026: No incidents
    • Sep 13, 2026: No incidents
    • Sep 14, 2026: No incidents
    • Sep 15, 2026: No incidents
    • Sep 16, 2026: No incidents
    • Sep 17, 2026: No incidents
    • Sep 18, 2026: No incidents
    • Sep 19, 2026: No incidents
    • Sep 20, 2026: No incidents
    • Sep 21, 2026: No incidents
    • Sep 22, 2026: No incidents
    • Sep 23, 2026: No incidents
    • Sep 24, 2026: No incidents
    • Sep 25, 2026: No incidents
    • Sep 26, 2026: No incidents
    • Sep 27, 2026: No incidents
    • Sep 28, 2026: No incidents
    • Sep 29, 2026: No incidents
    • Sep 30, 2026: No incidents
    • Oct 1, 2026: No incidents
    • Oct 2, 2026: No incidents
    • Oct 3, 2026: No incidents
    • Oct 4, 2026: No incidents
    • Oct 5, 2026: No incidents
    • Oct 6, 2026: No incidents
    • Oct 7, 2026: No incidents
    • Oct 8, 2026: No incidents
    • Oct 9, 2026: No incidents
    • Oct 10, 2026: No incidents
    • Oct 11, 2026: No incidents
    • Oct 12, 2026: No incidents
    • Oct 13, 2026: No incidents
    100.00% uptime
    Days with issues

    No incidents in this period.

Incidents

  • Elevated API latency

    Monitoring

    1. Monitoring. A fix is deployed. We are watching error rates.
    2. Investigating. Some requests are slower than usual.
  • Website outage

    Resolved

    1. Resolved. A bad deploy was rolled back.

Accessibility. Every day has a text label ("Jan 4, 2026: Outage, 42 minutes"). The strip is one tab stop; arrows pick a day and its text is printed beneath. A "Days with issues" disclosure lists only the bad days. Status is badge text, and bad days are striped.

Code
import { StatusPage } from '$lib/modules';

<StatusPage {components} {incidents} updatedAt="2026-10-13T09:00:00Z" />

ChangelogList

Releases newest first, each change tagged New, Improved, Fixed, Security, Breaking or Deprecated, with a type filter and anchor links.

Changelog

Show only

Showing 3 of 3 releases

  1. 2.1.0— Faster reports

    • New Scheduled report emails.
    • Improved Reports load twice as fast.
    • Fixed Fixed a blank chart in Safari.
  2. 2.0.1

    • Security Tightened session expiry.
    • Fixed Fixed duplicate invoice emails.
  3. 2.0.0— New dashboard

    • Breaking The v1 API is removed. See the migration guide.
    • New A redesigned dashboard.
    • Deprecated The legacy export format.

Accessibility. Each version heading is an anchor link with an aria-label. The filter is a checkbox group (checked chips show a tick). "Showing 2 of 5 releases" is a polite status. Tags include their word.

Code
import { ChangelogList } from '$lib/modules';

<ChangelogList {releases} bind:types />

WaitlistSignup

An email box that joins a waitlist, then shows your position and a referral link to share.

Join the waitlist

Be first in line when we open the doors.

Use an address starting with "fail" to see the error state.

Accessibility. The email field has autocomplete and a tied, announced error. After joining, focus moves to the confirmation heading and the position is a status. Copy link reports success in a live region.

Code
import { WaitlistSignup } from '$lib/modules';

<WaitlistSignup bind:state position={1204} referralCode="ada" shareUrl="https://example.com/" on:join={join} />

CouponBanner

A promo message with a copyable code and a countdown to the end. The clock is injectable.

Accessibility. The timer has aria-live="off"; the digits are hidden from assistive technology and a plain "2 days, 3 hours left" is provided. With reduced motion it updates once a minute and drops seconds. Nothing animates. Copy and dismiss are 44px buttons.

Code
import { CouponBanner } from '$lib/modules';

<CouponBanner message="Spring sale: 20% off" code="SPRING20" endsAt="2026-11-01T00:00:00Z" />