Header
Sticky site header with the Molly Ollys butterfly logo and wordmark. Pass appName to set the app label beside the brand name. Provide links to render inline nav links in the header bar. Optionally provide userName, userMessage, accountUrl, and signOutHref to render a user area on the right.
<Header appName="Portal" />
<!-- With inline nav links -->
<Header
appName="500 Club"
links={[
{ href: "#how-it-works", label: "How it works" },
{ href: "#faq", label: "FAQ" },
]}
/>SectionTitle
Small uppercase label for grouping content within a panel or card. Renders as an <h3> with secondary text colour.
Personal details
Address
<SectionTitle>Personal details</SectionTitle>
Horizontal Rule
Simple horizontal rule with vertical margins. Renders as an <hr> element styled with a light grey colour. Optional children are displayed centred between two rule lines.
Section above the rule
Section below the rule
<HorizontalRule />
Sign in with email
or
Continue with SSO
<HorizontalRule>or</HorizontalRule>
Badge
Inline status or category indicator. Seven colour variants plus a compact mode that renders uppercase with tighter padding.
Variants
Compact
<Badge variant="success">Active</Badge>
TagList
Renders a string[] as a row of Badge components. Pass any array of strings and it handles the layout. Supports onchipclick callback function and gap property, defaulting to '6px'.
<TagList tags={["Svelte", "TypeScript", "bits-ui"]} />Card
General-purpose surface container. Four variant options control shadow and border, and three padding sizes control inner spacing.
Variants
default
shadow-sm
elevated
shadow-md
prominent
shadow-strong
outlined
border, no shadow
Padding
padding sm
14px
padding md
20px (default)
padding lg
28px
<Card variant="elevated" padding="lg">Content</Card>
CardBlob
Stat/dashboard card with an animated pink blob that reveals on hover. Uses a frosted-glass inner layer over the bouncing blob. Hover the card to see the effect.
Supporters
Donations
<CardBlob> <h3>Supporters</h3> <span class="number">1,284</span> </CardBlob>
Table
Data-driven table with overflow scroll, sorting, and empty-state handling. Pass headers (with optional sort) and records as plain arrays. Provide onrowclick and onrowcontextmenu callbacks for row interactions. Sorting is managed internally. Override any cell by passing a snippet named cell_{key}.
Basic
| Name | Role | Status |
|---|---|---|
| Joe Herbert | Admin | Active |
| Bob Smith | Editor | Inactive |
| Carol White | Viewer | Active |
| Dan Brown | Editor | Pending |
<Table
headers={[{ label: "Name", key: "name", sort: true }, ...]}
records={rows}
onrowclick={(row) => console.log(row)}
/> With cell snippets
| Tags | |||
|---|---|---|---|
| Bob Smith | Editor | Inactive | |
| Carol White | Viewer | Active | |
| Dan Brown | Editor | Pending | |
| Joe Herbert | Admin | Active |
<Table {headers} {records}>
{#snippet cell_status(row)}
<Badge variant={statusVariant[row.status]}>{row.status}</Badge>
{/snippet}
{#snippet cell_tags(row)}
<TagList tags={row.tags} />
{/snippet}
</Table> Multi-select rows (shift+click)
| Bob Smith | Editor | Inactive |
| Carol White | Viewer | Active |
| Dan Brown | Editor | Pending |
| Joe Herbert | Admin | Active |
<Table
clickable
multiselect
{headers}
{records}
onrowclick={(row) => console.log(row)}
onrowcontextmenu={(rows) => console.log(rows)}
onselectionchange={(rows) => (selected = rows)}
bind:clearSelection
/>AccordionTable/AccordionTableGroup
Collapsible row for grouping a Table under a labelled header. Use AccordionTable on its own for a single group, or pass a tables array to AccordionTableGroup when stacking multiple — it provides the shared shadow and clips borders correctly. Provide meta strings for count/total metadata beside the title.
Single
<AccordionTable title="FY 2025/26" {headers} {records}>
{#snippet meta()}<span>4 donations</span><span>£2,400</span>{/snippet}
</AccordionTable> Grouped
<AccordionTableGroup
tables={[
{ title: "FY 2025/26", meta: ["4 donations", "£2,400"], headers, records },
{ title: "FY 2024/25", meta: ["2 donations", "£900"], headers, records },
]}
/>Accordion
Collapsible section with an animated chevron and slide transition. Bind open to control or read state externally, or leave it unbound for self-managed state. Stack multiple independently.
<Accordion title="Users (3)">
...content...
</Accordion>
<!-- Controlled -->
<Accordion title="Settings" bind:open={isOpen}>
...content...
</Accordion>AccordionDetailSection
Collapsible detail card with an animated chevron and slide transition. Bind open to control state externally, or leave it unbound for self-managed state. Pass a meta snippet to show supplementary text (e.g. a count or "(optional)") beside the title. Use fullWidth inside a grid to span all columns.
Basic and With Meta
<AccordionDetailSection title="Personal details">
<dl>...</dl>
</AccordionDetailSection>
<!-- With meta -->
<AccordionDetailSection title="Pledge">
{#snippet meta()}(optional){/snippet}
<dl>...</dl>
</AccordionDetailSection>DetailSection
Layout wrapper for detail/record views. Has an optional title field. Use fullWidth inside a grid to span all columns. Use card to wrap content in a --surface-alt colour card with padding and border.
Personal details
- Name
- Joe Herbert
- joe@example.com
- Phone
- 07700 900123
Address
- Line 1
- 12 Baker Street
- City
- London
- Postcode
- W1U 3BH
<DetailSection title="Personal details"> <dl>...</dl> </DetailSection> <DetailSection title="Address" card> <dl>...</dl> </DetailSection>
Form
Wrapper around the native <form> that surfaces SvelteKit action results. General errors appear as a banner below the form via result.message; per-field errors appear inline via result.fields and the companion FieldError component. Pass the SvelteKit enhance function via the enhance prop.
<Form method="POST" action="?/add" result={form} successMessage="Saved." enhance={(el) => enhance(el)}>
<Input name="name" placeholder="Name" />
<FieldError field="name" />
<Input name="email" type="email" placeholder="Email" />
<FieldError field="email" />
<Button type="submit" size="sm">Submit</Button>
</Form>FormItem
Labelled field wrapper. Pass label to render a <label> element that implicitly associates with any child input. Use required to append a required asterisk, span to control how many columns it occupies inside a FormSection grid, or fullWidth to stretch across all columns.
<FormSection cols={2}>
<FormItem label="First name">...</FormItem>
<FormItem label="Last name">...</FormItem>
<!-- span={2} stretches across a specific number of columns -->
<FormItem label="Email address" required span={2}>...</FormItem>
<!-- fullWidth always spans all columns regardless of count -->
<FormItem label="Notes" fullWidth>...</FormItem>
</FormSection>FormSection
Grid container for form items. Use cols for a fixed column count or minWidth for responsive auto-fit wrapping. gap controls spacing (default 8px). fullWidth makes it span all columns when nested inside another grid. Use card to wrap content in a --surface-alt colour card with padding and border.
Fixed columns — cols=3
Responsive auto-fit — minWidth=160
Nested — fullWidth inside cols=2
Card
<!-- Fixed columns -->
<FormSection cols={3}>
<FormItem label="Title">...</FormItem>
<FormItem label="First name" span={2}>...</FormItem>
<FormItem label="Email" span={3}>...</FormItem>
</FormSection>
<!-- Responsive: wraps when items would be narrower than 160px -->
<FormSection minWidth={160}>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</FormSection>
<!-- Nested: fullWidth spans parent grid, then defines its own -->
<FormSection cols={2}>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
<FormSection fullWidth cols={3}>
<FormItem label="Month">...</FormItem>
<FormItem label="Day">...</FormItem>
<FormItem label="Year">...</FormItem>
</FormSection>
</FormSection>
<!-- Card: wraps in --surface-alt background with border and padding -->
<FormSection cols={2} card>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</FormSection>FieldError
Renders a per-field validation message sourced from the nearest Form context. Pass field matching the input's name attribute; the component renders nothing when there is no error for that field. Must be used inside a Form component.
<Form result={form}>
<Input name="username" placeholder="Username" />
<FieldError field="username" />
</Form>Fieldset
Labelled form section with a pink legend. Pass title for the legend text, an optional actions snippet for buttons anchored to the top-right, and elements within the Fieldset for the body content.
Without actions
With actions
<Fieldset title="Personal details">
<Input name="first-name" placeholder="First name" />
</Fieldset>
<!-- With actions -->
<Fieldset title="Address">
{#snippet actions()}
<Button size="sm" variant="secondary">Edit</Button>
{/snippet}
...
</Fieldset>Input
Base text input. Accepts all standard HTML input attributes. Bind to value for two-way data binding. Supports placeholder, disabled, type, and any other native input prop.
<Input name="email" type="email" placeholder="you@example.com" bind:value />
Textarea
Multi-line text input sharing the same styles as Input. Accepts all standard textarea attributes. Use resize to control resize behaviour ("none", "both", "horizontal", "vertical"). Bind to value for two-way data binding.
<Textarea name="notes" rows=4 placeholder="Notes…" bind:value />
UnitInput
A text input with optional before and after adornments for units or labels. Accepts all standard input attributes. decimals controls how many decimal places to round to on blur. Bind to value for two-way data binding. Note that the value is always a string for flexibility with formatting and form serialisation; use the before/after props for unit display rather than including them in the value.
<UnitInput name="weight" before="kg" bind:value /> <UnitInput name="percentage" after="%" bind:value />
MoneyInput
GBP currency input with a £ prefix. Auto-formats to two decimal places on blur. Binds to a string value for use with form serialisation.
<MoneyInput name="price" bind:value />
FileInput
File picker with a Button trigger. Shows the selected filename (with extension) and size after selection. For multiple files, shows a count and total size. Supports accept for type filtering and multiple for multi-file selection.
Single file
Multiple files
Disabled
<FileInput label="Attachment" onchange={f => (files = f)} />
<!-- Multiple + type filter -->
<FileInput label="Documents" multiple accept=".pdf,.docx" buttonText="Choose Files" onchange={handler} />Checkbox
Accessible checkbox built on bits-ui. Supports two-way bind:checked, an optional description line beneath the label, and an onchange callback.
checked = false
<Checkbox name="accept" label="I agree" bind:checked />
Toggle
On/Off toggle switch. The sliding knob shows a tick or a cross and transitions between the pink palette states. Supports two-way bind:checked and an onchange callback.
checked = false
<Toggle name="feature" label="Enable feature" bind:checked />
ToggleGroup
Segmented button group built on bits-ui. Supports type="single" (at most one active) and type="multiple" (any number active). Set allowEmpty=true to allow deselecting the last active item. Controlled via value + onValueChange.
Single — allowEmpty (default)
value = week
Multiple — allowEmpty=false
value = [mon, wed]
<ToggleGroup type="single" {options} {value} allowEmpty={false} {onValueChange} />Select
Dropdown select built on bits-ui. Supports two-way bind:value and an onchange callback. Including an option with value: "Other" reveals a free-text input for custom values. clearable allows the user to reset the select.
<Select name="type" options={opts} bind:value clearable />SelectSearch
Fuzzy-search combobox for single selection. Type to filter options using uFuzzy. Clears cleanly when the input doesn't match any option. Useful for long option lists.
<SelectSearch name="country" options={opts} bind:value />MultiSelect
Multi-select trigger initially designed for filter bars. Shows a count badge when items are selected. Controlled via selected + onchange — ideal for syncing with URL search params or external filter state.
<MultiSelect label="Status" options={opts} {selected} {onchange} />MultiSelectSearch
Fuzzy-search combobox with multi-select. Type to filter options, click items to toggle selection — the dropdown stays open between picks. Shows a count in the placeholder and a ✕ clear button when items are selected. Pass showChips to render selected values as a TagList below the input, with an optional onChipClick callback.
Without chips
With chips + onChipClick
<MultiSelectSearch options={opts} {selected} {onchange} showChips {onChipClick} />DatePicker
Calendar date picker (GB locale) built on bits-ui. Binds to a YYYY-MM-DD string. Includes month/year dropdowns for fast navigation. Pairs with a hidden <input> for native form submission.
<DatePicker name="date" bind:value />
DateRangePicker
Range-only date picker for selecting a start and end date. Binds independently to startValue and endValue strings in YYYY-MM-DD format. Highlights the selected range in the calendar. Use DateSingleMultiPicker if you need a single/range toggle.
<DateRangePicker startName="from" endName="to" bind:startValue bind:endValue />
DateSingleMultiPicker
Composite picker that combines a ToggleGroup with DatePicker and DateRangePicker. The toggle switches between single-day and multi-day mode. In single-day mode only startValue is set; endValue is cleared. Bind multiDay to control or read the current mode.
mode = single
<DateSingleMultiPicker startName="from" endName="to" bind:startValue bind:endValue bind:multiDay />
Link
Styled anchor element. Renders pink with no underline by default; underline appears on hover. Supports all standard anchor props: href, target, rel, download, and class.
<Link href="/supporters/123">View supporter</Link> <!-- External --> <Link href="https://example.com" target="_blank" rel="noopener noreferrer">External</Link>
ConfirmDialog
Modal overlay dialog for confirmations. Requires a title, a message, and an onclose handler called when the backdrop is clicked. Action buttons go in the default slot.
<ConfirmDialog title="Delete?" message="This cannot be undone." onclose={close} open={showDialog}>
<Button variant="danger" onclick={confirm}>Delete</Button>
<Button variant="ghost" onclick={close}>Cancel</Button>
</ConfirmDialog>Toast
Non-blocking notification system. Call setToastState() once in your root layout and mount <Toast />. Then call getToastState() anywhere to fire toasts. Four severity variants with configurable auto-dismiss duration.
// root layout
setToastState();
<Toast />
// any component
const toast = getToastState();
toast.success("Saved!");Modal
Generic centered overlay dialog. Requires a title and an onclose handler. Content goes in the default slot. Optional width prop (default 560).
<Modal open={showModal} title="My Modal" onclose={() => (showModal = false)}>
<p>Modal content here.</p>
</Modal>SidePanel
Generic slide-in panel. Specify side ("left" or "right", default "right") and width in px (default 480). Clicking the overlay closes the panel. Used internally by DocumentViewerPanel and HistoryPanel.
<SidePanel bind:open width={400} side="right" ariaLabel="My panel">
...content...
</SidePanel>DocumentViewerPanel
Slide-in panel for previewing documents. Handles images, PDFs, video, audio, plain text, and CSV natively. Unsupported types fall back to a download prompt. Controlled via bind:open. Close by clicking the overlay or the ✕ button.
<DocumentViewerPanel bind:open src={url} filename="photo.jpg" mimeType="image/jpeg" downloadUrl={url} />
<DocumentViewerPanel bind:open src="/dummy-pdf.pdf" filename="dummy-pdf.pdf" mimeType="application/pdf" downloadUrl="/dummy-pdf.pdf" />HistoryPanel
Slide-in audit trail panel. Accepts either snapshot-based history (full JSON snapshots diffed against the previous version) or structured audit entries with explicit change lists. Each version row is collapsible. Controlled via bind:open.
<HistoryPanel bind:open {history} fieldLabels={{ first_name: "First name" }} />AddressAutofill
UK address lookup powered by Google Places. Calls /api/places/autocomplete as the user types (300 ms debounce, min 2 chars) then fetches full address details on selection. Fires onselect with a structured AutofilledAddress object containing line1, line2, city, county, postcode, country. Optional label prop overrides the field label (defaults to "Find address").
<AddressAutofill onselect={addr => console.log(addr)} />EntitySearch
Combobox for searching entities via an API endpoint. Fetches results from endpoint?q=… as the user types. Supports single-select (bind:value → UUID string) and multi-select (multiple + bind:selected → UUID array) modes. Use currentLabel to pre-populate the display label for an existing value on mount. Callbacks: onselect(uuid, option) for single mode, onmultiselect(uuids) for multi mode. Optional label, placeholder, and required props.
<!-- Single-select -->
<EntitySearch bind:value endpoint="/api/supporters" label="Supporter" currentLabel="Jo Smith" onselect={handleSelect} />
<!-- Multi-select -->
<EntitySearch multiple bind:selected endpoint="/api/supporters" label="Supporters" onmultiselect={handleMultiSelect} />