Molly Ollys UI Kit
Current Version: v1.12.1
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. The bottom bar doubles as a page loader — call setPageLoaderState() in your root layout and use pageLoader.start() / pageLoader.stop() from any component to animate it. Pass loaderColor to override the loader's colour (defaults to --pink).
Pass logoUrl to render an additional logo (e.g. an event or org logo) in the brand area alongside the butterfly and wordmark — it doesn't replace either. Use the boolean props showButterfly, showWordmarkMolly, and showWordmarkApp (all default true) to independently hide the butterfly icon, the "Molly Ollys" wordmark, or the app-name wordmark. The left snippet renders after the wordmark, for inserting custom content (e.g. a badge or short label) into the brand area. The right snippet renders at the far end of the header row, after the nav links, user area, and burger menu, for inserting custom content (e.g. an action button or icon) that should always stay visible.
Pass padding to override the header's default padding (12px 5vw) with any CSS padding shorthand value.
<Header appName="Portal" />
<!-- With inline nav links -->
<Header
appName="500 Club"
links={[
{ href: "#how-it-works", label: "How it works" },
{ href: "#faq", label: "FAQ" },
]}
/>
<!-- Page loader — call setPageLoaderState() once in your root layout alongside Header.
The Header reads it from context automatically. -->
// root layout (+layout.svelte)
setPageLoaderState();
// any component
const pageLoader = getPageLoaderState();
pageLoader.start(); // animate the bar
pageLoader.stop(); // return to static line
<!-- Custom loader colour -->
<Header appName="Portal" loaderColor="#3b82f6" />
<!-- Custom logo alongside (or instead of) the default brand assets.
logoUrl never replaces showButterfly/showWordmark* — each is controlled independently. -->
<Header appName="Portal" logoUrl={event.logoUrl} showWordmarkApp={false} />
<Header appName="Portal" logoUrl={event.logoUrl} showButterfly={false} showWordmarkMolly={false} showWordmarkApp={false} />
<!-- left snippet: insert content into the brand area, after the wordmark.
right snippet: insert content at the far end of the header row, after the
nav links, user area, and burger menu. -->
<Header appName="Portal">
{#snippet left()}
<span class="badge">Beta</span>
{/snippet}
{#snippet right()}
<Button size="sm" variant="ghost">Help</Button>
{/snippet}
</Header>
<!-- Custom padding (overrides the default 12px 5vw) -->
<Header appName="Portal" padding="24px 5vw" />Burger
Animated hamburger/close toggle used by Header for its collapsed nav. Supports two-way bind:open and an onchange callback. Pass label to customise the accessible name (announced as "Open/Close {label}").
open = false
<Burger bind:open label="menu" />
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>
LayoutItem
Grid cell wrapper for non-labeled content inside a LayoutSection. Renders a plain <div> with no label — use it for buttons, headings, or any content that needs grid placement without a form label. Supports the same span and fullWidth props as FormItem. Use title to render a SectionTitle above the content, and actions to anchor a snippet of buttons to the top-right corner. Use card to wrap content in a Card component, or detailCard to apply a --surface-alt background with border and padding.
<LayoutSection cols={2}>
<FormItem label="First name">...</FormItem>
<FormItem label="Last name">...</FormItem>
<!-- LayoutItem for non-labeled content that needs grid placement -->
<LayoutItem fullWidth>
<Button type="submit">Save</Button>
</LayoutItem>
</LayoutSection> Personal details
- Name
- Joe Herbert
- joe@example.com
- Phone
- 07700 900123
Address
- Line 1
- 12 Baker Street
- City
- London
- Postcode
- W1U 3BH
<!-- With title only -->
<LayoutItem title="Personal details">
<dl>...</dl>
</LayoutItem>
<!-- card: wraps content in a Card component -->
<LayoutItem title="Address" card>
{#snippet actions()}
<Button size="sm" variant="secondary">Edit</Button>
{/snippet}
<dl>...</dl>
</LayoutItem>
<!-- detailCard: --surface-alt background with border/padding; actions anchored top-right -->
<LayoutItem title="Notes" card detailCard>
<p>Some content</p>
</LayoutItem>LayoutSection
Grid container for laying out content. 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. Add vertical to stack items top-to-bottom: sets flex-direction: column in flex mode, or grid-auto-flow: column when cols/minWidth is set. Use title to render a SectionTitle above the grid, and required to mark it as required. Use detailCard to apply a --surface-alt background with border and padding, card to wrap the section in a Card component, or dashed for a dashed border only.
Fixed columns — cols=3
Responsive auto-fit — minWidth=160
Nested — fullWidth inside cols=2
detailCard
card
dashed
vertical (flex, no cols/minWidth)
title
Address
title + required
Address
<!-- Fixed columns -->
<LayoutSection cols={3}>
<FormItem label="Title">...</FormItem>
<FormItem label="First name" span={2}>...</FormItem>
<FormItem label="Email" span={3}>...</FormItem>
</LayoutSection>
<!-- Responsive: wraps when items would be narrower than 160px -->
<LayoutSection minWidth={160}>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</LayoutSection>
<!-- Nested: fullWidth spans parent grid, then defines its own -->
<LayoutSection cols={2}>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
<LayoutSection fullWidth cols={3}>
<FormItem label="Month">...</FormItem>
<FormItem label="Day">...</FormItem>
<FormItem label="Year">...</FormItem>
</LayoutSection>
</LayoutSection>
<!-- detailCard: --surface-alt background with border and padding -->
<LayoutSection cols={2} detailCard>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</LayoutSection>
<!-- card: wraps in a Card component -->
<LayoutSection cols={2} card>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</LayoutSection>
<!-- dashed: dashed border only -->
<LayoutSection cols={2} dashed>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</LayoutSection>
<!-- vertical: flex-direction:column (no cols/minWidth), or grid-auto-flow:column (with cols/minWidth) -->
<LayoutSection vertical>
<FormItem label="First name">...</FormItem>
<FormItem label="Last name">...</FormItem>
</LayoutSection>
<!-- title: renders a SectionTitle above the grid -->
<LayoutSection cols={2} title="Address">
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</LayoutSection>
<!-- required: marks the title as required -->
<LayoutSection cols={2} title="Address" required>
<FormItem label="City">...</FormItem>
<FormItem label="Postcode">...</FormItem>
</LayoutSection>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>
BadgeList
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'.
<BadgeList 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}. Pass a footer record to render a summary row in <tfoot> — it uses the same keys as headers and supports the same cell snippets.
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)}
<BadgeList 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
/> With footer row
| Item | Quantity | Amount |
|---|---|---|
| Ticket — Adult | 3 | £75.00 |
| Ticket — Child | 2 | £30.00 |
| Donation | 1 | £50.00 |
| Total | 6 | £155.00 |
<Table
{headers}
{records}
footer={{ item: "Total", quantity: 6, amount: "£155.00" }}
/>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. Pass a meta snippet for supplementary text beside the title. Use fullWidth inside a grid to span all columns. Add large for a bigger card-style variant with a bolder header.
Default
<Accordion title="Personal details">
<dl>...</dl>
</Accordion>
<!-- With meta -->
<Accordion title="Pledge">
{#snippet meta()}(optional){/snippet}
<dl>...</dl>
</Accordion> Large
<Accordion large title="Users (3)">
...content...
</Accordion>
<!-- Controlled -->
<Accordion large title="Settings" bind:open={isOpen}>
...content...
</Accordion>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. Renders a <label> that implicitly associates with any child input. Pass label for the label text and required to append a required asterisk. Use span to control how many columns it occupies inside a LayoutSection grid, or fullWidth to stretch across all columns. For non-labeled content that still needs grid placement, use LayoutItem instead.
<LayoutSection 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>
</LayoutSection>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. Set clearable to show a clear button once a value is entered.
<Input name="email" type="email" placeholder="you@example.com" bind:value /> <Input name="search" placeholder="Search…" clearable 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. Set clearable to show a clear button once a value is entered.
<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 />ColorPicker
Swatch-and-popover colour picker with a saturation/brightness field, hue slider, and preset swatches. Supports two-way bind:value and an onchange callback. Set alpha to enable an opacity slider and translucent output. Use formats to choose which text representations (hex, rgb, hsl) are selectable — any recognised format is accepted as input regardless of which is active. Pass a custom presets array to override the default swatches. Set freeText to allow arbitrary CSS in the text field (keywords like red, linear-gradient(…), var(--token), etc.) — the picker still tries to sync the visual controls when it can resolve a solid colour from the text. Pass initialValue to set the colour the reset button (next to the text field) restores — it defaults to the starting value when omitted, and is disabled while the current colour already matches it. Set showReset to false to hide the reset button entirely. An eyedropper button is shown next to the text field when the browser supports the EyeDropper API — set eyedropper to false to disable it.
value = #ea3796
value = #3b82f6cc
formats={["rgb", "hsl"]} — value = rgb(23 162 184)
freeText — value = rebeccapurple
initialValue="#3b82f6" — value = #ea3796
disabled
<ColorPicker name="brand-color" bind:value alpha formats={["hex", "rgb", "hsl"]} freeText initialValue="#3b82f6" />Slider
Range input with a label and live value. Supports two-way bind:value, min/max/step, and an onchange callback. Set showValue=false to hide the value readout, or pass formatValue to customise how it's displayed.
<Slider name="brightness" label="Brightness" bind:value min={0} max={100} />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 BadgeList below the input, with an optional onChipClick callback. Pass allowCustom to let users add a value that isn't in options — an Add "…" item appears in the dropdown when the typed text doesn't match an existing option or selection. Pass onCustomAdd to be notified when such a value is added.
Without chips
With chips + onChipClick
With allowCustom
<MultiSelectSearch options={opts} {selected} {onchange} showChips {onChipClick} allowCustom {onCustomAdd} />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 />
DateTimePicker
Like DatePicker, but also captures a time of day. Binds to a YYYY-MM-DDTHH:mm string (24-hour) by default. Picking a date from the calendar keeps the previously entered time. The hour/minute (and optional second) fields are typeable and support wrapping mouse-wheel scrolling, with left/right arrow keys moving between them. Set seconds to also capture seconds (YYYY-MM-DDTHH:mm:ss). Set timezone to let the user pick an IANA timezone via a searchable list — value becomes a plain offset ISO string (e.g. 2024-06-01T10:30:00+01:00, still safe to pass to new Date()), and the zone identifier itself (e.g. Europe/London) is tracked separately via bind:zone.
<DateTimePicker name="datetime" bind:value /> <DateTimePicker name="datetime" seconds timezone bind:value bind:zone />
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 />
DateTimeRangePicker
Range date picker with an independent time-of-day for each endpoint. Binds to startValue and endValue strings in YYYY-MM-DDTHH:mm format (add seconds for YYYY-MM-DDTHH:mm:ss). Each endpoint has its own typeable/scrollable time spinner (or native time input on touch devices).
<DateTimeRangePicker 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>Loader
Loading indicators with eight visual variants controlled by the variant prop (default spinner). The spinner variant uses the --border and --pink-dark CSS variables. All variants accept a size prop (px) that sets the width — square variants (pulse, layers, image) scale height automatically via aspect-ratio. Accepts all standard <span> attributes — pass aria-label and aria-live for accessible indicators.
backlit (dark bg only) <Loader />
<!-- Named variant -->
<Loader variant="pulse" />
<Loader variant="backlit" />
<Loader variant="layers" />
<Loader variant="text" />
<Loader variant="graph" />
<Loader variant="line" />
<Loader variant="image" />
<!-- Custom size — sets width on all variants -->
<Loader size={32} />
<Loader variant="pulse" size={64} />
<!-- image variant: custom background colour (default: --surface-alt) -->
<Loader variant="image" bg="var(--pink-light)" />
<!-- Accessible saving indicator -->
<Loader aria-label="Saving…" aria-live="polite" />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. Pass a position as the first argument to setToastState() — defaults to "top-right". Top positions sit just below the header; use the --header-height CSS variable to match your header's height (defaults to 60px).
// root layout
setToastState();
<Toast />
// any component — position is optional, defaults to "top-right"
const toast = getToastState();
toast.success("Saved!", "bottom-right");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. Pass an actions snippet to render buttons in the header beside the close button. Pass a footer snippet to render a bordered footer bar below the body, styled to match the header. Used internally by DocumentViewerPanel and HistoryPanel.
<SidePanel bind:open width={400} side="right" title="My panel">
...content...
</SidePanel>
<!-- With header actions -->
<SidePanel bind:open title="Edit record">
{#snippet actions()}
<Button size="sm" variant="ghost">Export</Button>
{/snippet}
...content...
</SidePanel>
<!-- With footer -->
<SidePanel bind:open title="Edit record">
...content...
{#snippet footer()}
<Button size="sm" type="submit">Save</Button>
<Button size="sm" variant="ghost" onclick={() => (open = false)}>Cancel</Button>
{/snippet}
</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} />ThemeSync
Non-visual utility component that syncs a theme preference ("light", "dark", or "system") to localStorage and the root element class. Mount it once in your root layout and bind your theme state to the theme prop. The dark class on <html> activates dark-mode CSS variable overrides from theme.css.
active = system
<!-- root +layout.svelte -->
<script>
import { ThemeSync } from "@molly-ollys/ui-kit";
let theme = $state<"light" | "dark" | "system">("system");
</script>
<ThemeSync {theme} />
<slot />ThemeToggle
Visual three-way theme control (Light / System / Dark) bound to the shared themeController singleton. Clicking an option applies it immediately (toggling the dark/light class on <html>), mirrors it to localStorage and a first-party mollyolly_theme cookie, syncs other tabs of the same app via BroadcastChannel, and — when the app has wired ThemeSync with a saveAccountTheme callback and the user is logged in — persists it to the account. It needs no props; mount ThemeSync once in your root layout for the sync/persistence wiring, then drop ThemeToggle anywhere you want an in-app control (account page, header, footer). Pass label to override the legend text.
active = system
<!-- root +layout.svelte: wire sync once -->
<script>
import { ThemeSync, ThemeToggle } from "@molly-ollys/ui-kit";
</script>
<ThemeSync
theme={data.user?.themePreference}
loggedIn={!!data.user}
{fetchAccountTheme}
{saveAccountTheme}
/>
<!-- anywhere: the in-app control -->
<ThemeToggle label="Theme" />SortState
Reactive class for managing external (e.g. server-side) sort state. Holds field and dir as Svelte 5 $state properties. Call toggle(field) to cycle direction or switch fields; read indicator(field) for a sort arrow string. Pass the pre-sorted records to Table and set sort=false on headers to disable the table's built-in sorting.
| Name | Role | Status |
|---|---|---|
| Bob Smith | Editor | Inactive |
| Carol White | Viewer | Active |
| Dan Brown | Editor | Pending |
| Joe Herbert | Admin | Active |
sort = name asc
import { SortState } from "@molly-ollys/ui-kit";
const sort = new SortState<"name" | "date">("name", "asc");
// Read sort state to pass to your API
$effect(() => {
fetchData({ sortBy: sort.field, sortDir: sort.dir });
});
// Render a sortable column header
<button onclick={() => sort.toggle("name")}>
Name {sort.indicator("name")}
</button>
// Pass pre-sorted rows — disable Table's built-in sort
<Table headers={[{ label: "Name", key: "name" }]} {records} />