Molly Olly's Wishes
Molly Ollys UI Kit

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

Default Success Danger Warning Pink Muted Neutral

Compact

Default Success Danger Warning
<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'.

Svelte 5TypeScriptbits-uiAccessibleThemeable
<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

1,284 total supporters
47 regular giving

Donations

£84k this year
£71k last year
<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

NameRoleStatus
Joe HerbertAdminActive
Bob SmithEditorInactive
Carol WhiteViewerActive
Dan BrownEditorPending
<Table
  headers={[{ label: "Name", key: "name", sort: true }, ...]}
  records={rows}
  onrowclick={(row) => console.log(row)}
/>

With cell snippets

Tags
Bob SmithEditorInactive
Carol WhiteViewerActive
Volunteer
Dan BrownEditorPending
NewsletterEvents
Joe HerbertAdminActive
TrusteeMajor donor
<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 SmithEditorInactive
Carol WhiteViewerActive
Dan BrownEditorPending
Joe HerbertAdminActive
<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
Email
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

Personal details

With actions

Address
Line 1
12 Baker Street
City
London
Postcode
W1U 3BH
<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.

kg
%
~ mph
<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

Attachment
No file chosen

Multiple files

Documents
No file chosen

Disabled

Upload
No file chosen
<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 />

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} />

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.

ddmmyyyy
<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.

ddmmyyyy

mode = single

<DateSingleMultiPicker startName="from" endName="to" bind:startValue bind:endValue bind:multiDay />

Button

General-purpose action button. Renders as <button> by default, or as <a> when href is provided. Supports five visual variants, two sizes, and a disabled state.

Variants

Sizes & states

Link Button
<Button variant="secondary" size="sm" onclick={handler}>Label</Button>

SearchButton

Expands into a text input on click. Fires a debounced onsearch callback (300 ms) as the user types. Collapses back when blurred with an empty value.

<SearchButton bind:value onsearch={handler} placeholder="Search…" />

CopyButton

Inline icon button that copies a string to the clipboard. Swaps to a checkmark for 2 seconds after copying. Accepts an optional oncopy callback — use it to fire a toast. The title prop sets the tooltip.

Standalone

joe@example.com

With toast feedback

07700 900123
<CopyButton text={value} title="Copy email" oncopy={() => toast.success("Copied.")} />

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!");

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)} />