UI Kit

Button

Reusable <Button> with a TS-typed variant union. This page is the canonical reference — pick the variant whose intent matches, not whose style looks close.

When to use each variant
  • primary — single main CTA per surface. One per view.
  • secondary — alternate action at similar visual weight (e.g. "Export" alongside "Save").
  • ghost — cancel / escape hatch. Use alongside primary or danger so the confirm action reads first and cancel recedes.
  • danger — destructive confirms only (delete, revoke). Never for reversible actions.
  • default — neutral glass button; standalone actions with no clear CTA hierarchy.
  • iconOnly — compact icon-only. Non-danger = ghost icon (light gray hover). variant="danger" = outlined red circle.
Filled variants
With icon
Disabled
Icon only
Close
Edit
Delete

Checkbox

Native <input type="checkbox"> styled via accent-color. Add role="switch" when the semantic is on/off. Backing CSS lives in src/app.css — no Svelte component needed.

States
role="switch" (on/off semantic)

Badge

pill (default) for muted status markers; chip for colored category tags. tone picks the color; icon adds a leading glyph; width fixes column width for grid alignment.

Pill (status markers)
current active warning error
Chip (category tags)
ui db auth fix feat
With icon
improved saved

IconBadge

An icon with an optional numeric count overlay — use for notification dots on nav items or anywhere a count needs to sit on top of an icon. The dot is hidden when count is 0 or omitted. Capped visually at "99+". The badge is aria-hidden; supply accessible context via the parent element's label.

With count
No count (dot hidden)

NoticeBanner

An informational strip with a leading icon, body text, and an optional call-to-action. Use tone="muted" (default) for static warnings or configuration notices; tone="accent" for actionable prompts. Provide href + cta to render the banner as a link.

Muted (default) — static notice
Integration not configured. Set EXAMPLE_TOKEN in the server environment to enable this feature.
Accent — actionable prompt
6 entries from last week pending sync Sync now →

Toasts

Auto-dismiss after 4 s by default. Use success for completed mutations, error for failures (always with a message), info for neutral notices. Toast is mounted once in the root layout — don't remount it.

Skeleton

Inline loading placeholder shown inside <svelte:boundary> pending snippets. Appears after a 150 ms delay so sub-threshold loads never flash. Use radius="circle" for avatars, height + width to match the content it replaces.

Spinner

Inline loading indicator for in-flight mutations (button disabled + spinner). Inherits currentColor so it adapts to button variants. Prefer Skeleton for content areas; reserve Spinner for buttons and small status indicators.

0.9 em (default) 1.4 em 2 em

Tooltip

Uses the CSS popover="hint" API with interestfor — no JS event listeners. Only works on interactive elements (<button>, <a>). Appears after a brief interest delay; top-end aligns the right edge of the tooltip to the right edge of the trigger.

Tooltip (left)
Tooltip (top)
Tooltip (bottom)
Tooltip (top-end)
Tooltip (right)

Pagination

Self-hiding when totalPages ≤ 1. Bind page and pass totalPages; optionally add pageSize + total for an item-range label. Page clamping is handled internally.

ProjectIcon

Renders a color swatch when no iconUrl is set; falls back to an <img> when one is provided. size controls both dimensions. Default size is 40 px.

No icon (color only)
color: #f59e0b
color: #10b981
color: #ef4444
size=24
size=56
iconUrl (image)

Modal

Slide-up dialog — header + scrollable body + optional footer. Parent owns open; onclose fires on × / ESC / backdrop.

Props
  • actions snippet — footer buttons. Omit for body-inline actions.
  • busy — disables × and sets aria-busy. Handle ESC/backdrop in onclose to suppress them during long ops.
  • Bottom-sheet on mobile (<640px).
Variants

Example Modal

Body content goes here. Anything you put as a child of <Modal> renders inside the body.

The footer below comes from the actions snippet.

No footer

When actions is omitted, the footer disappears entirely. Useful when actions live inline in the body (e.g. an "+ Add" button at the end of a list).

Busy state

While busy is truthy, the × button is disabled and the dialog gets aria-busy="true". ESC and backdrop-tap also close the dialog — you usually want to suppress those during long ops too (handle in the parent's onclose).

Tabs

Underlined link-tabs nav. Active tab is computed from page.url.pathname against each href; pass an array of { href, label, testId? }. The component owns its own margin-bottom — don't wrap in spacing utilities. The UI Kit's own top nav (Components / Organisms / Tokens / Charts) uses this primitive.

Disclosure

Inline collapsible <details> with an animated chevron. Strips the global card treatment so it reads as auxiliary copy, not a panel. Use when you want to hide reference text behind a small "When to use" affordance — e.g. the Button variant guide above.

Show details
  • Disclosure is for inline help text, not panels — it shouldn't host primary actions.
  • Use the title as a read-out, not a question — "When to use each variant", not "Need help?".
  • Pair with a parent that owns its own padding; the Disclosure has none.

AiButton

Round outlined sparkle button — the visual primitive for any "Improve with AI" trigger. Pure presentational; the consumer owns the click handler and any preview/diff panel. Pass shimmer on lone surfaces (a single textarea) to draw attention; leave it off on dense lists (a 100-row changelog) where animation would be noise. Pass pressed to show the active "panel open" treatment.

Variants
default
Improve with AI
shimmer
Improve with AI
pressed
Improve with AI
disabled
Improve with AI

LimitedTextarea

Textarea that visualizes how close the input is to its character limit. Bar is inset inside the textarea bottom (green → amber → red, red zone reserved for the final ~8%) and a numeric counter sits below. max is required; all other textarea props are forwarded. maxlength defaults to max so the browser truncates pasted input at the cap. Screen readers get the count via aria-describedby linking the textarea to the counter element.

Live demo (160-char tweet limit)
0 / 160

Manage Projects

Delete project?

Delete this project? This cannot be undone.

Import Data

Edit Entry (NaNh NaNm)

Details

Duration

Delete entry?

This cannot be undone.

Manual Entry