A compact, modular UI toolkit built on Svelte 5 with styling powered by TailwindCSS and a clean layer of custom CSS variables. The library is engineered around a simple idea: components should be predictable, lightweight and transparent inside. No framework gymnastics, no slot jungles, no global side effects. Every piece stands alone and behaves exactly as written.
The visual system is driven by Tailwind, but core design tokens live in app.css as CSS variables. This keeps the theme consistent, avoids scattered magic numbers and makes dark mode trivial: add .dark to any parent and the whole UI switches instantly. Styles stay portable, the bundle stays small, and components donβt pull in hidden global utilities.
Svelte 5 gives the library a clean execution model: state is explicit, updates are deterministic, composition is simple, and the output is easy to reason about. Snippets replace legacy slots and avoid wrapper hierarchies that typically bloat component libraries.
The toolkit is built for engineers: no hidden behavior, no opaque abstractions, no vendor lockβjust straightforward components you can read, modify and trust. Everything plays nicely with Vite, Storybook, strict TypeScript, unit tests and real-world SPA workflows where clarity and maintainability matter more than magic.
$state, $derived, $effect, $props).dark classsrc/app.css for consistent theming# clone
git clone https://github.com/MaestroFusion360/svelte-comp.git
cd svelte-comp
# install
npm i
# dev / build / preview
npm run dev
npm run build
npm run preview
scripts/ # Scripts
src/
βββ demo/ # Demo components
βββ lib/ # Component library
β βββ __tests__/ # Component tests
β βββ types/ # Component types
β βββ *.svelte # Component files
β βββ index.ts # Public exports
βββ stories/ # Storybook stories
βββ utils/ # Utility functions
βββ App.svelte # Demo application
βββ lang.ts # Localization
βββ app.css # Theme tokens (CSS variables)
All tokens below map 1:1 to the variables in src/app.css.
Values under "Dark" are overrides applied inside .dark { β¦ }.
All tables below stay unchanged - verified against app.css.
| Token | Light | Dark |
|---|---|---|
--color-text-default |
oklch(15% 0 0deg) |
oklch(98% 0 0deg) |
--color-text-muted |
oklch(60% 0 0deg) |
oklch(50% 0 0deg) |
--color-text-danger |
oklch(92% 0.05 25deg) |
oklch(98% 0.02 25deg) |
--color-text-success |
oklch(92% 0.05 150deg) |
oklch(92% 0.05 150deg) |
--color-text-warning |
oklch(95% 0.05 90deg) |
oklch(95% 0.05 90deg) |
--color-text-link |
oklch(35% 0.3 250deg) |
oklch(65% 0.3 250deg) |
--color-text-red |
oklch(50% 0.25 30deg) |
oklch(50% 0.25 30deg) |
| Token | Light | Dark |
|---|---|---|
--color-bg-page |
oklch(98% 0 0deg) |
oklch(15% 0 0deg) |
--color-bg-surface |
oklch(100% 0 0deg) |
oklch(26% 0 0deg) |
--color-bg-primary |
oklch(62.3% 0.214 259.8deg) |
oklch(62.3% 0.214 259.8deg) |
--color-bg-secondary |
oklch(80% 0 0deg) |
oklch(45% 0 0deg) |
--color-bg-danger |
oklch(60% 0.25 30deg) |
oklch(50% 0.25 30deg) |
--color-bg-success |
oklch(55% 0.2 150deg) |
oklch(45% 0.2 150deg) |
--color-bg-warning |
oklch(75% 0.2 70deg) |
oklch(65% 0.2 70deg) |
--color-bg-muted |
oklch(90% 0 0deg) |
oklch(30% 0 0deg) |
| Token | Light | Dark |
|---|---|---|
--color-bg-hover |
oklch(94% 0 0deg) |
oklch(25% 0 0deg) |
--color-bg-active |
oklch(88% 0 0deg) |
oklch(18% 0 0deg) |
| Token | Light | Dark |
|---|---|---|
--border-color-default |
oklch(85% 0 0deg) |
oklch(35% 0 0deg) |
--border-color-strong |
oklch(75% 0 0deg) |
oklch(45% 0 0deg) |
--border-color-focus |
oklch(68.7% 0.14 237.5deg) |
oklch(68.7% 0.14 237.5deg) |
| Token | Light | Dark |
|---|---|---|
--shadow-color |
oklch(0% 0 0deg / 0.15) |
oklch(0% 0 0deg / 0.6) |
| Token | Value |
|---|---|
--spacing-xs |
0.25rem |
--spacing-sm |
0.5rem |
--spacing-md |
1rem |
--spacing-lg |
1.5rem |
--spacing-xl |
2rem |
| Token | Value |
|---|---|
--font-sans |
"Inter", -apple-system, BlinkMacSystemFont, sans-serif |
--font-mono |
"Fira Code", "Consolas", "Monaco", monospace |
| Token | Value |
|---|---|
--font-weight-normal |
400 |
--font-weight-medium |
500 |
--font-weight-semibold |
600 |
--font-weight-bold |
700 |
| Token | Value |
|---|---|
--text-xs |
0.75rem |
--text-sm |
0.875rem |
--text-md |
1rem |
--text-lg |
1.125rem |
--text-xl |
1.25rem |
| Token | Value |
|---|---|
--line-height-tight |
1.1 |
--line-height-normal |
1.4 |
--line-height-relaxed |
1.6 |
--letter-spacing-tight |
-0.01em |
--letter-spacing-normal |
0 |
--letter-spacing-wide |
0.02em |
| Token | Value |
|---|---|
--radius-sm |
0.125rem |
--radius-md |
0.375rem |
--radius-lg |
0.5rem |
--radius-xl |
0.75rem |
--radius-2xl |
1rem |
--radius-full |
9999px |
| Token | Value |
|---|---|
--transition-fast |
150ms |
--transition-normal |
300ms |
--transition-slow |
500ms |
--timing-default |
ease-in-out |
| Token | Light | Dark |
|---|---|---|
--opacity-disabled |
0.5 |
0.4 |
--opacity-hover |
0.9 |
0.85 |
--opacity-overlay |
0.7 |
0.6 |
| Token | Value |
|---|---|
--z-base |
0 |
--z-dropdown |
10 |
--z-overlay |
50 |
--z-modal |
100 |
--z-toast |
200 |
Collapsible content container with flexible sizing and optional multi-open behavior.
items?: AccordionItem[] - Array of sections { id?, title, content } (default: [])multiple?: boolean - Allow more than one section to be open at the same time (default: false)defaultOpen?: number[] - Indexes of initially opened sections (default: [])onToggle?: (index: number, open: boolean) => void - Callback fired when a section is toggledsz?: SizeKey - Size variant (xs|sm|md|lg|xl) (default: md)class?: string - Additional classes for the outer container (default: "")<script lang="ts">
import Accordion from '$lib/Accordion.svelte';
const items = [
{ title: 'First', content: 'This is the first item' },
{ title: 'Second', content: 'This is the second item' },
{ title: 'Third', content: 'This is the third item' }
];
const handleToggle = (index: number, open: boolean) => {
console.log(index, open);
};
</script>
<Accordion {items} multiple defaultOpen={[0]} sz="md" onToggle={handleToggle} />
Versatile button supporting multiple variants, sizes, loading state, and link behavior.
disabled?: boolean - Disables interaction (default: false)children?: Snippet - Content rendered inside the buttononClick?: (e: MouseEvent) => void - Click handlersz?: SizeKey - Button size variant (xs|sm|md|lg|xl) (default: md)variant?: ButtonVariant - Visual style preset (primary|secondary|pill|danger|success|warning|ghost|link|info) (default: primary)type?: "button" | "submit" | "reset" - Button type attribute (default: "button")loaded?: boolean - Shows loading spinner and blocks clicks (default: false)link?: string - Navigates to a URL when clickedclass?: string - Additional classes for the button (default: "")disabled and loaded both prevent click events.loaded is true.target and rel attributes.aria-disabled and aria-busy states.<script lang="ts">
import Button from '$lib/Button.svelte';
</script>
<Button onClick={() => console.log('clicked')}>
Save
</Button>
<Button variant="danger" loaded>
Deleting
</Button>
<Button link="/about" variant="link">
Navigate
</Button>
Flexible layout component with optional header and footer sections. Supports predefined size variants (xs to xl) through the sz prop.
header?: Snippet - Content rendered in the card headerfooter?: Snippet - Content rendered in the card footerchildren?: Snippet - Main body content of the cardclass?: string - Additional CSS classes for the card (default: "")sz?: SizeKey - Padding and typography preset (xs|sm|md|lg|xl) (default: md)flushHeader?: boolean - Removes padding and border from the header (default: false)flushFooter?: boolean - Removes padding and border from the footer (default: false)--color-bg-surface, --border-color-default).sz.{@render} snippets instead of legacy slots.<script lang="ts">
import Card from '$lib/Card.svelte';
</script>
{#snippet header()}
<h2 class="font-semibold text-center">Card Header</h2>
{/snippet}
{#snippet footer()}
<p class="text-sm text-center text-[var(--color-text-muted)]">
Β© 2025 MaestroFusion360
</p>
{/snippet}
<Card {header} {footer} sz="md">
<p>Main content of the card.</p>
</Card>
A responsive carousel component to display a sequence of items with optional autoplay, navigation arrows, and dots.
items?: CarouselItem[] - Array of carousel items (default: [])sz?: SizeKey - Size variant controlling text scale and rounding (xs|sm|md|lg|xl) (default: md)autoplay?: boolean - Enables automatic slide rotation (default: false)interval?: number - Interval between slides in milliseconds (default: 5000)showDots?: boolean - Shows navigation dots (default: true)showArrows?: boolean - Shows navigation arrows (default: true)class?: string - Additional classes for the carousel container (default: "")Card.svelte internally for slide structure.aria-label, aria-current, and keyboard focus on controls.<script lang="ts">
import Carousel from '$lib/Carousel.svelte';
import type { CarouselItem } from '$lib/types';
const items: CarouselItem[] = [
{ title: 'Item 1', content: 'Content 1', image: 'image1.jpg' },
{ title: 'Item 2', content: 'Content 2', image: 'image2.jpg' }
];
</script>
<Carousel {items} autoplay interval={3000} showDots showArrows />
Accessible custom checkbox with indeterminate support.
label?: string - Text label shown next to the checkboxsz?: SizeKey - Size option (xs|sm|md|lg|xl) (default: md)variant?: ComponentVariant - Visual style preset (default|neutral) (default: default)indeterminate?: boolean - Enables the mixed state (default: false)checked?: boolean - Controlled checked state (bindable) (default: false)onChange?: (checked: boolean) => void - Fired when the checkbox togglesclass?: string - Extra classes applied to the root container (default: "")invalid?: boolean - Marks the field invalid and sets aria-invalid (default: false)describedBy?: string - ID of helper or error text for accessibilitybind:checked; onChange receives the final boolean.indeterminate is applied to the underlying input and reported as aria-checked="mixed".indeterminate clears it and sets checked=true.invalid maps to aria-invalid; describedBy maps to aria-describedby.variant (neutral uses border color).xs β xl).<script lang="ts">
import CheckBox from '$lib/CheckBox.svelte';
let agree = false;
</script>
<CheckBox
label="I agree"
bind:checked={agree}
onChange={(v) => (agree = v)}
/>
<CheckBox
label="Partially selected"
indeterminate
variant="neutral"
/>
CodeView is a small prism.js powered code block that supports syntax highlighting, optional editing, line numbers and active-line highlighting.
code?: string - Code content to render (default: "")language?: Language - Syntax highlighting language (txt|html|css|js|json|python) (default: "txt")title?: string - Title displayed above the code block (default: "Code")showCopyButton?: boolean - Shows the copy-to-clipboard button (default: true)showLineNumbers?: boolean - Displays line numbers alongside the code (default: false)editable?: boolean - Enables editable mode with a textarea overlay (default: false)activeLine?: boolean - Highlights the current cursor line in editable mode (default: false)sz?: SizeKey - Size preset affecting spacing and typography (xs|sm|md|lg|xl) (default: md)sz token.<script lang="ts">
import CodeView from '$lib/CodeView.svelte';
let snippet = `<div class="box">Hello</div>`;
let editable = true;
</script>
<CodeView
title="Example"
language="html"
bind:code={snippet}
{editable}
showLineNumbers
activeLine
/>
Accessible wrapper around the native <input type="color"> with a trigger button and preview card.
value?: string | null - Selected color value (hex) (default: null)label?: string - Label displayed above the controlplaceholder?: string - Placeholder text when no color is chosendisabled?: boolean - Disables all interactions (default: false)clearable?: boolean - Shows a clear/reset button (default: true)onChange?: (value: string | null) => void - Fired when the color changesclass?: string - Additional classes for the wrapper element (default: "")HTMLInputElement.showPicker() API when available; falls back to focusing/clicking the hidden input.value through $effect.aria-label.clearable=false hides the clear button; when disabled, pointer/keyboard handlers are skipped.<script lang="ts">
import ColorPicker from '$lib/ColorPicker.svelte';
let accent: string | null = null;
</script>
<ColorPicker
label="Brand color"
placeholder="Pick a hue"
bind:value={accent}
/>
<p>Preview: {accent ?? 'No color selected'}</p>
Button-driven date selector that formats the chosen value and supports min/max limits.
value?: string | null - Selected date value (ISO YYYY-MM-DD) (default: null)min?: string - Minimum selectable date (ISO YYYY-MM-DD)max?: string - Maximum selectable date (ISO YYYY-MM-DD)label?: string - Label text displayed above the pickerplaceholder?: string - Placeholder shown when no date is selectedlocale?: string - Locale used for formattingformatOptions?: Intl.DateTimeFormatOptions - Custom date formatting optionsdisabled?: boolean - Disables all interactions (default: false)clearable?: boolean - Shows a clear button to reset the value (default: true)onChange?: (value: string | null) => void - Fired when the date changesclass?: string - Additional classes for the wrapper element (default: "")<input type="date"> so native date-pickers, keyboard, and validation work automatically.showPicker() is used when available for a consistent trigger; fallback is focus + click.Intl.DateTimeFormat (with locale/formatOptions) and gracefully falls back to toLocaleDateString().clearable resets the underlying input value and dispatches onChange(null).aria-live="polite" to announce updated dates.<script lang="ts">
import DatePicker from '$lib/DatePicker.svelte';
let launchDate: string | null = null;
</script>
<DatePicker
label="Launch date"
min="2025-01-01"
max="2025-12-31"
bind:value={launchDate}
/>
<p>Scheduled for: {launchDate ?? 'TBD'}</p>
Modal dialog for confirmations or alerts.
open?: boolean - Controls dialog visibility (default: false)title?: string - Dialog title used for labeling (default: "")message?: string - Simple message content (default: "")onConfirm?: () => void - Fired when the confirm action is triggered (default: () => {})onCancel?: () => void - Fired when the cancel action is triggered (default: () => {})onClose?: () => void - Fired after confirm or cancel to centralize cleanup (default: () => {})modal?: boolean - Enables modal mode with overlay and focus trap (default: true)class?: string - Extra classes applied to the dialog container (default: "")sz?: SizeKey - Size preset for padding and text (xs|sm|md|lg|xl) (default: md)children?: Snippet - Custom dialog body contentEscape triggers cancel.onClose runs after onConfirm/onCancel, so you can centralize cleanup.modal=false) renders a floating panel without overlay or focus trap.children for full custom UI.title for accessibility; it's used as the dialog's aria-label.sz adjusts both dialog padding and text sizes to match the rest of the system tokens.<script lang="ts">
import Dialog from '$lib/Dialog.svelte';
import Button from '$lib/Button.svelte';
let open = false;
</script>
<Button onClick={() => (open = true)}>
Delete
</Button>
<Dialog
{open}
sz="sm"
title="Delete item"
message="This action cannot be undone."
onConfirm={() => { open = false; }}
onCancel={() => (open = false)}
onClose={() => (open = false)}
/>
Unified input/textarea field with label, leading/trailing content, clear button, and validation.
as?: "input" | "textarea" - Underlying element to render (default: "input")label?: string - Label text rendered above the fieldsz?: SizeKey - Size preset for spacing and typography (xs|sm|md|lg|xl) (default: md)variant?: FieldVariant - Visual style variant (default|filled|neutral) (default: default)clearable?: boolean - Shows a clear button for text inputs (default: true)rows?: number - Row count for textarea mode (default: 3)parseNumber?: boolean - Coerces numeric input when possible (default: false)leading?: Snippet | string - Leading content rendered inside the fieldtrailing?: Snippet | string - Trailing content rendered inside the fieldonChange?: (val: string | number) => void - Fired when the value changesvalue?: string | number - Controlled field value (bindable) (default: "")class?: string - Additional classes applied to the root label (default: "")id?: string - Custom id used for label and input linkagetype?: string - Input type when as="input"invalid?: boolean - Marks the field invalid and sets aria-invalid (default: false)describedBy?: string - ID of helper or error text for accessibilitybind:value is supported; onChange receives cast value (number when parseNumber succeeds, otherwise string or "").type="number") and sets value to an empty string.id.aria-invalid, aria-describedby; number inputs also set inputmode="decimal".<script lang="ts">
import Field from '$lib/Field.svelte';
let q = '';
let description = '';
let amount: number | '' = '';
</script>
<Field label="Search" placeholder="Type hereβ¦" bind:value={q} />
<Field
as="textarea"
label="Description"
rows={4}
bind:value={description}
/>
<!-- Leading as a string -->
<Field
label="Amount"
type="number"
parseNumber
bind:value={amount}
leading="$"
/>
<!-- Trailing as a snippet -->
{#snippet percent()}
%
{/snippet}
<Field
label="Discount"
trailing={percent}
/>
Lightweight file selector with click support and drag-and-drop. Internally uses a hidden <input type="file"> plus a drop zone.
accept?: string - Accepted file types (default: "*\\/*")multiple?: boolean - Allows selecting multiple files (default: false)label?: string - Button label; falls back to localized textdisabled?: boolean - Disables all interactions (default: false)clearable?: boolean - Shows a clear button to reset selection (default: true)placeholder?: string - Placeholder text for the drop zonevalue?: FileList | null - Controlled selected files (bindable) (default: null)onFilesSelected?: (files: FileList | null) => void - Fired when files are chosenonError?: (error: string) => void - Fired on validation errorsclass?: string - Additional classes for the wrapper (default: "")accept does not apply to dropped files, only to the picker UI; validate files inside onFilesSelected.clearable=true, the user can clear selected files and the callback receives null.disabled=true, clicks, drag events, focus, and keyboard input are blocked.<script lang="ts">
import FilePicker from '$lib/FilePicker.svelte';
function handleFiles(files: FileList | null) {
if (!files) {
console.log('Cleared');
return;
}
const names = Array.from(files).map(f => f.name);
console.log('Selected:', names);
}
</script>
<FilePicker
accept="image/*,.pdf"
multiple
label="Upload files"
onFilesSelected={handleFiles}
/>
Declarative, schema-driven form generator. Renders Field, Select, and CheckBox based on FieldSchema. Supports validation, controlled state, and an external API via expose.
schema?: FieldSchema[] - Field configuration for the generated formvalue?: FormValues - Initial form data (default: {})rowGap?: number | SizeKey - Vertical spacing between fields (default: "md")validateOn?: "input" | "blur" | "submit" - When validation should run (default: "blur")onChange?: (form: FormValues) => void - Fired when form values changeonChange?: (form: FormValues) => void - Fired when form values changeformId?: string - Stable identifier for form elementsexpose?: (api: FormApi) => void - Exposes imperative Form APIlabelAlign?: AlignText - Alignment for labels (left|center|right) (default: "left")labelWeight?: LabelWeight - Font weight for labels (normal|medium|semibold|bold) (default: "medium")labelSize?: SizeKey - Size preset for labels (xs|sm|md|lg|xl) (default: "md")compact?: boolean - Enables denser sizing across controls (default: false)value[name] β schema.default β '' (or false for checkboxes).validateOn='input'|'blur'|'submit' controls when validators run; built-in checks: required, number, and email regex.when(form) hides a field dynamically; hidden fields are skipped during validation.Select options are coerced to strings for the underlying control; provide string values if you rely on strict equality.ids and wired via aria-describedby; invalid flags are passed to inputs.expose provides { reset, submit, validate, getData }; validate returns Promise<boolean>.compact reduces control sizes (xsβxs, smβxs, mdβsm, lgβmd, xlβlg) and centers labels where applicable.<script lang="ts">
import Form from '$lib/Form.svelte';
import type { FieldSchema } from '$lib/types';
const schema: FieldSchema[] = [
{ name: 'email', type: 'email', label: 'Email', required: true,
validators: [(v) => (!v ? 'Required' : null)] },
{ name: 'password', type: 'password', label: 'Password', required: true },
{ name: 'remember', type: 'checkbox', label: 'Remember me', default: true },
{ name: 'plan', type: 'select', label: 'Plan',
options: [{ value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }], default: 'free' },
{ name: 'bio', type: 'textarea', label: 'Bio', rows: 3, when: (f) => f.plan === 'pro' }
];
let api: { submit: () => void } | undefined;
const grabExpose = (x: any) => { api = x; };
</script>
<Form
{schema}
onSubmit={(form) => console.log(form)}
onChange={(form) => console.debug(form)}
expose={grabExpose}
/>
<Button onClick={() => api?.submit()}>
Submit
</Button>
Off-canvas navigation drawer controlled by a floating hamburger button.
menuItems?: Item[] - Menu entries rendered in the drawer (default: [])activeItem?: string - ID of the currently active item (default: "")header?: Snippet - Custom content rendered above the menufooter?: Snippet - Custom content rendered below the menucloseOnSelect?: boolean - Automatically closes after selecting an item (default: true)onSelect?: (id: string) => void - Fired when a menu item is chosenonOpenChange?: (v: boolean) => void - Fired when open state changes in controlled modepressed?: boolean - Controlled open stateclass?: string - Extra classes applied to the trigger button (default: "")width?: number | string - Drawer width (px or CSS value) (default: 300)Escape closes the drawer.pressed is defined), state changes are requested via onOpenChange(open).menuItems is empty, a βNo itemsβ placeholder is shown.role=\"dialog\" and aria-modal=\"true\"; the trigger reflects state via aria-expanded.<script lang="ts">
import Hamburger from '$lib/Hamburger.svelte';
const menuItems = [
{ id: 'home', label: 'Home' },
{ id: 'settings', label: 'Settings' }
];
const header = () => 'Main navigation';
const footer = () => 'Β© 2025 Widgets Inc.';
</script>
<Hamburger
{menuItems}
activeItem="home"
header={header}
footer={footer}
onSelect={(id) => console.log('navigate', id)}
/>
A dropdown menu bar component with hover and click interactions.
menus?: MenuItem[] - Menu definitions with actions (default: [])onSelect?: (menu: string, action: MenuAction) => void - Fired when an action is chosen (default: () => {})class?: string - Extra classes applied to the menu bar (default: "")sz?: SizeKey - Size preset for spacing and text (xs|sm|md|lg|xl) (default: sm)xs, sm, md, lg, xl) are automatically highlighted to reflect the current UI size.menus structure and onSelect.<script lang="ts">
import Menu from '$lib/Menu.svelte';
import type { MenuItem } from '$lib/types';
const menus: MenuItem[] = [
{
name: 'File',
actions: [
'New',
'Open',
{ id: 'save', label: 'Save' },
{ id: 'sm', label: 'Small UI' }
]
},
{
name: 'Edit',
actions: ['Undo', 'Redo', 'Copy', 'Paste']
}
];
function handleSelect(menu: string, action: string) {
console.log('Selected:', menu, action);
}
</script>
<Menu
{menus}
sz="sm"
onSelect={handleSelect}
/>
A card component with built-in pagination. Renders items page by page inside a Card and appends Pagination in the footer.
items?: Snippet[] - Array of renderable snippets for each item (default: [])itemsPerPage?: number - Items per page (must be >= 1) (default: 1)header?: Snippet - Optional Card header contentfooter?: Snippet - Custom footer content shown above paginationclass?: string - Extra classes passed to the underlying Card (default: "")currentPage state (starts at 1).totalPages is clamped to at least 1; empty items still yields one page.footer snippet renders before it.Pagination.svelte internally with { currentPage, totalPages, onPageChange }.itemsPerPage must be >= 1; smaller values are not supported.<script lang="ts">
import PaginatedCard from '$lib/PaginatedCard.svelte';
import type { Snippet } from 'svelte';
const items: Snippet[] = Array.from({ length: 7 }, (_, i) => () => `Item ${i + 1}`);
const header: Snippet = () => 'Item list';
const footer: Snippet = () => 'Total: 7';
</script>
<PaginatedCard {items} {header} {footer} itemsPerPage={3} class="max-w-xl" />
Compact pagination component for table or list navigation.
currentPage?: number - The active page number (1-based)totalPages?: number - Total number of pages availableonPageChange?: (page: number) => void - Fired when a page button is clickedclass?: string - Custom classes applied to the pagination wrapper (default: "")aria-current=\"page\" on the active page for accessibility.<button> elements for keyboard support.<script lang="ts">
import Pagination from '$lib/Pagination.svelte';
let currentPage = 1;
const totalPages = 10;
function handlePageChange(page: number) {
currentPage = page;
}
</script>
<Pagination
{currentPage}
{totalPages}
onPageChange={handlePageChange}
/>
A simple and accessible progress bar component that visually represents task completion.
value?: number - Current progress value from 0 to 100 (default: 0)indeterminate?: boolean - Enables the animated indeterminate state (default: false)sz?: SizeKey - Controls the bar height (xs|sm|md|lg|xl) (default: md)variant?: ComponentVariant - Visual style of the progress bar (default|neutral) (default: default)class?: string - Additional CSS classes for the wrapper element (default: "")label?: string - Optional text label displayed above the bar (default: "")disabled?: boolean - Applies a muted inactive visual style (default: false)aria roles.<script lang="ts">
import ProgressBar from '$lib/ProgressBar.svelte';
let progress = 65;
</script>
<ProgressBar value={progress} label="Loading..." />
<ProgressBar value={progress} variant="neutral" disabled />
<ProgressBar indeterminate label="Syncing..." />
Single choice input with optional label, custom sizing and theme variants.
label?: string - Text label shown next to the radiochildren?: Snippet - Custom label contentsz?: SizeKey - Size option (xs|sm|md|lg|xl) (default: md)variant?: ComponentVariant - Visual style preset (default|neutral) (default: default)checked?: boolean - Controlled checked state (default: false)group?: unknown - Native radio group binding (bind:group)onChange?: (checked: boolean) => void - Fired when the radio togglesclass?: string - Extra classes applied to the root container (default: "")describedBy?: string - ID of helper or error text for accessibilityvalue?: string - Radio value (default: "on")bind:groupchecked and onChange are used togetherchildren takes priority over label<script lang="ts">
import { Radio } from '$lib';
let fruit = $state('banana');
</script>
<div class="flex flex-col gap-2">
<Radio value="apple" bind:group={fruit}>Apple</Radio>
<Radio value="banana" bind:group={fruit}>Banana</Radio>
<Radio value="cherry" bind:group={fruit}>Cherry</Radio>
</div>
Accessible custom combobox with label, portal listbox, hidden form input, and configurable sizing.
label?: string - Field label rendered above the triggerhelp?: string - Optional helper copy rendered below the fieldplaceholder?: string - Text shown when nothing is selectedoptions?: SelectOption[] - Available options ({ label, value, disabled? }) (default: [])sz?: SizeKey - Sizing preset (xs|sm|md|lg|xl) (default: md)variant?: FieldVariant - Surface style preset (default|filled|neutral) (default: default)value?: string - Selected value (bindable) (default: "")onChange?: (val: string) => void - Fired when a new option is chosenclass?: string - Extra classes for the root <label> (default: "")id?: string - Custom id for the fieldinvalid?: boolean - Shows invalid state and sets aria-invalid (default: false)describedBy?: string - Links to helper or error text idsopen?: boolean - Controlled dropdown visibility (bindable) (default: false)maxHeight?: number - Max dropdown height before scrollingArrow, Home/End, Enter/Space, looped Tab) with roving tabindex buttons inside a listbox.position: fixed math to stay aligned with the trigger (including scroll/resize listeners).<input type=\"hidden\"> mirrors bind:value so forms and contextual BaseField integrations keep working.onChange.<script lang="ts">
import Select from '$lib/Select.svelte';
const flavorOptions = [
{ label: 'Vanilla', value: 'vanilla' },
{ label: 'Chocolate', value: 'choco' },
{ label: 'Coming Soon', value: 'mint', disabled: true }
];
let flavor = '';
let isMenuOpen = false;
</script>
<Select
label="Favorite flavor"
placeholder="Pick one"
help="Disabled items mean we ran out :("
options={flavorOptions}
bind:value={flavor}
bind:open={isMenuOpen}
variant="filled"
onChange={(val) => console.log('selected', val)}
/>
A customizable slider component for selecting a value from a range.
value?: number - The current value (bindable) (default: 0)min?: number - Minimum value (default: 0)max?: number - Maximum value (default: 100)step?: number - Step size (default: 1)sz?: SizeKey - Slider size (xs|sm|md|lg|xl) (default: md)variant?: ComponentVariant - Color variant (default|neutral) (default: default)disabled?: boolean - Disable the slider (default: false)showValue?: boolean - Show the current value (default: false)onInput?: (value: number) => void - Fires on value changeclass?: string - Custom wrapper classes (default: "")<script lang="ts">
import Slider from '$lib/Slider.svelte';
let value = 50;
</script>
<Slider bind:value={value} min={0} max={100} step={5} showValue />
Resizable split panel container with horizontal or vertical orientation.
direction?: 'horizontal' | 'vertical' - Split orientation (horizontal|vertical) (default: horizontal)initialSize?: number - Initial size of the first panel as percentage (default: 30)dividerSize?: number - Width/height of the divider handle in pixels (default: 4)minSize?: number - Minimum size of the first panel as percentage (default: 10)maxSize?: number - Maximum size of the first panel as percentage (default: 90)first?: Snippet - Content for the first panelsecond?: Snippet - Content for the second panel<script lang="ts">
import Splitter from "$lib/Splitter.svelte";
</script>
{#snippet first()}
<div
class="p-[var(--spacing-lg)] bg-[var(--color-bg-surface)] text-[var(--color-text-default)] h-full"
>
First
</div>
{/snippet}
{#snippet second()}
<div
class="p-[var(--spacing-lg)] bg-[var(--color-bg-surface)] text-[var(--color-text-default)] h-full"
>
Second
</div>
{/snippet}
<div
class="
fixed inset-0
bg-[var(--color-bg-page)]
flex items-center justify-center
"
>
<div class="w-[80vw] h-[80vh] flex flex-col">
<!-- Horizontal -->
<div class="h-[45%] border-[var(--border-color-default)] rounded-[var(--radius-xl)] overflow-hidden mb-[var(--spacing-xl)]">
<Splitter
direction="horizontal"
initialSize={50}
dividerSize={8}
minSize={15}
maxSize={85}
{first}
{second}
/>
</div>
<!-- Vertical -->
<div class="h-[55%] border-[var(--border-color-default)] rounded-[var(--radius-xl)] overflow-hidden">
<Splitter
direction="vertical"
initialSize={40}
dividerSize={8}
minSize={15}
maxSize={85}
{first}
{second}
/>
</div>
</div>
</div>
A compact toggle switch component built on top of a native <input type="checkbox">. Supports optional labels around the control and works naturally with bind:checked.
sz?: SizeKey - Size preset for the control (xs|sm|md|lg|xl) (default: md)checked?: boolean - Current state (bindable) (default: false)leftLabel?: string - Optional label rendered on the left siderightLabel?: string - Optional label rendered on the right sidetopLabel?: string - Optional label placed above the switchonChange?: (v: boolean) => void - Fired on toggle with the new valueclass?: string - External wrapper classes (default: "")disabled by dimming visuals and removing pointer events.checked value, so it's predictable in forms and controlled UI.<script lang="ts">
import Switch from '$lib/Switch.svelte';
let enabled = false;
</script>
<Switch
topLabel="Notifications"
leftLabel="Off"
rightLabel="On"
bind:checked={enabled}
onChange={(v) => (enabled = v)}
/>
<p>Current state: {enabled ? 'on' : 'off'}</p>
Sortable table with optional zebra striping, sticky header, and external pagination. Compact variants (dense, list) shrink horizontally to fit content.
columns?: readonly Column<T>[] - Column definitions with labels and keys (default: [])rows?: readonly T[] - Row data objects (default: [])class?: string - Custom classes for the table container (default: "")variant?: TableVariant - Visual style variant (default|dense|list|noBorder|noTitle|zebra) (default: default)stickyHeader?: boolean - Makes the header row sticky (default: false)sz?: SizeKey - Size preset for spacing and text (xs|sm|md|lg|xl) (default: md)sz?: SizeKey - Size preset for spacing and text (xs|sm|md|lg|xl) (default: md)dense and list the table uses content width (inline-table + w-fit).width is applied only in non-compact variants (default, zebra, noBorder, noTitle).list hides the header row.Pagination.svelte via the pagination prop.<script lang="ts">
import Table from '$lib/Table.svelte';
import type { Column } from '$lib/types';
type Row = { name: string; age: number; city: string };
const columns: Column<Row>[] = [
{ key: 'name', label: 'Name', align: 'start' },
{ key: 'age', label: 'Age', align: 'end', width: '80px' },
{ key: 'city', label: 'City', align: 'start' }
];
const rows: Row[] = [
{ name: 'Ada', age: 36, city: 'London' },
{ name: 'Linus', age: 55, city: 'Helsinki' }
];
</script>
<Table {columns} {rows} variant="zebra" stickyHeader />
<!-- Compact list variant -->
<Table {columns} {rows} variant="list" />
A tab navigation component for switching between sections of content.
tabs?: TabItem[] - Array of tab items with id and label (default: [])activeTab?: string - The currently active tab id (default: "")sz?: SizeKey - Size preset for tabs and content (xs|sm|md|lg|xl) (default: md)variant?: TabsVariant - Visual style of the tabs (default|pills|underline) (default: default)fitted?: boolean - Stretches tabs to fill available width (default: false)onChange?: (tabId: string) => void - Callback when the active tab changesclass?: string - Custom class for the container (default: "")children?: Snippet - Content panel rendered below the tabsdefault, pills, underline).children) scales visually with the selected tab size.<script lang="ts">
import Tabs from '$lib/Tabs.svelte';
import type { TabsVariant, SizeKey } from '$lib/types';
let activeDefault = 't1';
let sz: SizeKey = 'md';
let variant: TabsVariant = 'default';
</script>
<Tabs
tabs={[
{ id: 't1', label: 'One' },
{ id: 't2', label: 'Two' },
{ id: 't3', label: 'Three' },
]}
{sz}
{variant}
activeTab={activeDefault}
onChange={(id) => (activeDefault = id)}
>
{#if activeDefault === 't1'}
<div class="py-8">One content</div>
{/if}
{#if activeDefault === 't2'}
<div class="py-8">Two content</div>
{/if}
{#if activeDefault === 't3'}
<div class="py-8">Three content</div>
{/if}
</Tabs>
Lightweight theme switcher to toggle between light and dark mode. Applies or removes the .dark class on the document root.
disabled?: boolean - Disable the ThemeToggleclass?: string - Optional external class name (overrides default position) (default: "")sz?: SizeKey - Adjusts button size and icon scale (xs|sm|md|lg|xl) (default: md)type?: string - Button type attribute (default: "button")$effect to sync the dark class on <html>.fixed top-4 right-4, unless overridden by a custom class.<script lang="ts">
import ThemeToggle from '$lib/ThemeToggle.svelte';
</script>
<!-- Default (top-right) -->
<ThemeToggle />
<!-- Custom position and size -->
<ThemeToggle class="fixed bottom-4 right-4 z-50" sz="lg" />
Behavior: Click to switch between light and dark modes. The button updates its icon automatically (sun in dark mode, moon in light mode).
Simple time selector that stores values in ISO HH:MM format. Supports a fixed step and two display systems.
value?: string | null - Stored time in ISO HH:MM (bindable) (default: null)step?: number - Step in seconds (default: 60)label?: string - Label textplaceholder?: string - Placeholder when value is nulldisabled?: boolean - Disable all interactions (default: false)clearable?: boolean - Show the clear button (default: true)initialSystem?: "iso" | "english" - Picker mode (24h vs 12h) (default: "iso")onChange?: (value: string | null) => void - Fired when value changesclass?: string - Wrapper classes (default: "")HH:MM)step defines the minute grid (derived from seconds)<script lang="ts">
import TimePicker from '$lib/TimePicker.svelte';
let time: string | null = null;
</script>
<TimePicker
label="Pick a time"
step={300}
bind:value={time}
initialSystem="english"
/>
<p>Stored: {time ?? 'None'}</p>
Lightweight notification component for transient messages.
title?: string - Optional title displayed above the messagemessage?: string - Toast message contentvariant?: ToastVariant - Visual style (success|danger|warning|info) (default: info)showIcon?: boolean - Shows an icon matching the variant (default: true)closable?: boolean - Shows a close button (default: true)timeout?: number - Auto-hide timeout in milliseconds (default: 3000)onClose?: () => void - Fired when the toast closes (default: () => {})class?: string - Additional wrapper classes (default: "")timeout.closable = true.<script lang="ts">
import Toast from '$lib/Toast.svelte';
let show = false;
function notify() {
show = true;
setTimeout(() => (show = false), 3000);
}
</script>
<button onclick={notify}>Show toast</button>
{#if show}
<Toast
title="Saved"
message="Settings updated"
variant="success"
onClose={() => (show = false)}
/>
{/if}
Context-aware hint for inline controls and labels.
children?: Snippet - Inline trigger contenttext?: string - Tooltip textposition?: "top" | "bottom" | "left" | "right" - Tooltip placement (default: "top")delay?: number - Delay before showing the tooltip (ms) (default: 300)open?: boolean - Forces visibility when true (default: false)class?: string - Wrapper classes (default: "")text.open overrides hover/focus behavior when set to true.position controls which side of the trigger the bubble appears on.delay adds a small pause before showing the tooltip to avoid flicker.class is applied to the root wrapper, useful for layout tweaks.<script lang="ts">
import Tooltip from "$lib/Tooltip.svelte";
import Button from "$lib/Button.svelte";
let forced = false;
</script>
<Tooltip text="Primary action button">
{#snippet children()}
<Button sz="sm" variant="primary">
Save
</Button>
{/snippet}
</Tooltip>
<Tooltip text="Forced tooltip" open={forced}>
{#snippet children()}
<button onclick={() => (forced = !forced)}>
Toggle tooltip
</button>
{/snippet}
</Tooltip>
$state, $derived, $effect, $props)..dark.{@render}.# Development
npm run dev # Vite dev server
npm run storybook # Storybook on :6006
# Code Quality
npm run check # TypeScript checking
npm run lint # ESLint validation
npm run lint:fix # Auto-fix lint issues
npm run format # Prettier formatting
# Testing
npm run test # Vitest unit tests
npm run test:watch # Vitest watch mode
npm run test:ui # Vitest UI interface
# Docs
npm run md src/lib # Generate Components.md from all Svelte components
Testing Stack:
Β· Vitest + Testing Library - Unit tests with DOM testing Β· jsdom - Browser environment simulation Β· Coverage via @vitest/coverage-v8
Code Quality:
Β· ESLint with TypeScript + Svelte plugins Β· Prettier for consistent formatting Β· TypeScript strict type checking
Storybook:
Β· Component documentation & testing Β· Accessibility addons (@storybook/addon-a11y) Β· Vitest integration (@storybook/addon-vitest)
MIT License - See LICENSE for details.