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.
- UI Components Library (Svelte 5 + TailwindCSS)
- ✨ Features
- 🚀 Quick Start
- 📦 Package
- 📁 Project Structure
- 🎨 Global Styles (Theme Tokens)
- Theme Tokens - Text Colors
- Theme Tokens - Backgrounds
- Theme Tokens - Interaction States
- Theme Tokens - Borders
- Theme Tokens - Shadow
- Theme Tokens - Spacing
- Theme Tokens - Typography Families
- Theme Tokens - Font Weights
- Theme Tokens - Text Sizes
- Theme Tokens - Line Height and Letter Spacing
- Theme Tokens - Radius
- Theme Tokens - Transitions
- Theme Tokens - Opacity
- Theme Tokens - Z-Index
- 🧱 Components
- Accordion.svelte
- Badge.svelte
- Button.svelte
- Calendar.svelte
- Card.svelte
- Carousel.svelte
- CheckBox.svelte
- CodeView.svelte
- ColorPicker.svelte
- ContextMenu.svelte
- DatePicker.svelte
- Dialog.svelte
- Field.svelte
- FilePicker.svelte
- Form.svelte
- Hamburger.svelte
- InstallPWA.svelte
- Menu.svelte
- NoticeBase.svelte
- PaginatedCard.svelte
- Pagination.svelte
- PrimaryColorSelect.svelte
- ProgressBar.svelte
- ProgressCircle.svelte
- Radio.svelte
- SearchInput.svelte
- Select.svelte
- Slider.svelte
- Splitter.svelte
- Switch.svelte
- Table.svelte
- Tabs.svelte
- ThemeToggle.svelte
- TimePicker.svelte
- TimePickerNew.svelte
- Toast.svelte
- Tooltip.svelte
- Topbar.svelte
- 🎯 Design Principles
- 🧪 Testing & Development
- 📄 License
- 💡 Built on Svelte 5 Runes API (
$state,$derived,$effect,$props) - 🎨 Styled with TailwindCSS v4 using CSS variables
- 🌗 Full dark mode support via
.darkclass - 🧩 Completely self-contained components (no global dependencies)
- ⚙️ Composable API – props and functional children instead of slots
- 🪶 Minimal bundle size and zero runtime styling overhead
- 🧱 Clear design tokens defined in
src/app.cssfor consistent theming - 🌍 Built-in internationalization with support for English, Russian, and Spanish
# 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
npm i svelte-compInstall Tailwind and the Vite plugin:
npm i tailwindcss @tailwindcss/viteEnable it in vite.config.ts / vite.config.js:
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [
tailwindcss(),
svelte(), // Must be after tailwindcss()
],
});Add global styles in src/app.css (or main stylesheet):
@import "tailwindcss";
@import "svelte-comp/styles.css";npm i prismjs @types/prismjsscripts/ # Scripts
src/
|-- demo/ # Demo components and demo apps (Notepad, Calculator, Todo List)
| `-- __tests__/ # Demo app tests
|-- 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 |
Expand on
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:"")
- Smooth expand/collapse via CSS grid transitions
- Accessible triggers (button + aria-expanded)
- For full ARIA compliance, pair triggers with aria-controls and hide collapsed panels using aria-hidden or inert
- State is index-based; reordering items will change which panels start open
- AccordionItem.content is a plain string; wrap HTML inside the string or fork the component if you need custom nodes
<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} />Compact status badge for inline labels and small indicators.
message?: string- Badge textvariant?: ToastVariant- Visual style (success|danger|warning|info) (default:info)showIcon?: boolean- Shows a variant icon (default:false)class?: string- Additional wrapper classes (default:"")
- Intended for inline status labels.
- Uses the same variant palette as Toast.
<script lang="ts">
import Badge from '$lib/Badge.svelte';
</script>
<Badge message="Active" variant="success" showIcon />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:"")
disabledandloadedboth prevent click events.- Automatically shows a centered spinner when
loadedistrue. - Link navigation supports
targetandrelattributes. - Accessible with
aria-disabledandaria-busystates. - The component uses CSS variables for colors, spacing, and transitions.
<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>Monthly calendar grid with navigation and date selection.
value?: string | null- Selected date in ISOYYYY-MM-DD(bindable) (default:null)min?: string- Minimum selectable date (ISOYYYY-MM-DD)max?: string- Maximum selectable date (ISOYYYY-MM-DD)locale?: string- Locale used for month/day labels (default:"en-US")weekStartsOn?: 0|1|2|3|4|5|6- First day of week (0=Sun ... 6=Sat) (default:1)showOutsideDays?: boolean- Render days from adjacent months (default:true)disabled?: boolean- Disables selection and navigation (default:false)onChange?: (value: string | null) => void- Fired on date selectionclass?: string- Additional classes for the root wrapper (default:"")
- Selection value is always ISO
YYYY-MM-DD. - Month labels and weekday names are localized using
Intl.DateTimeFormat. - Days outside the current month can be shown or hidden via
showOutsideDays. - Uses CSS variables for spacing, colors, and focus styles.
<script lang="ts">
import Calendar from '$lib/Calendar.svelte';
let selected: string | null = null;
</script>
<Calendar bind:value={selected} weekStartsOn={1} />
<p>Selected: {selected ?? 'None'}</p>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)
- Theme-aware: uses CSS variables (
--color-bg-surface,--border-color-default). - Rounded corners, subtle shadow, and border for a clean visual hierarchy.
- Padding and typography scale automatically with
sz. - Uses
{@render}snippets instead of legacyslots.
<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:"")
- Supports touch gestures (swipe left/right).
- Autoplay pauses automatically when unmounted.
- Uses
Card.svelteinternally for slide structure. - Navigation dots and arrows appear only if there’s more than one item.
- Accessible via
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 setsaria-invalid(default:false)describedBy?: string- ID of helper or error text for accessibility
- Fully bindable via
bind:checked;onChangereceives the final boolean. indeterminateis applied to the underlying input and reported asaria-checked="mixed".- Clicking the custom box while
indeterminateclears it and setschecked=true. invalidmaps toaria-invalid;describedBymaps toaria-describedby.- SVG check and dash are inline; colors adapt per
variant(neutraluses border color). - Sizes scale the control box (
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)class?: string- Extra classes applied to the root container (default:"")
- Uses Prism for syntax highlighting; HTML/CSS/TXT grammars are bundled by default.
- Editable mode renders a transparent textarea above a highlighted code layer, mirroring real editors.
- Cursor-line highlight is overlaid using CSS variables and scroll-position tracking.
- Line numbers are fully scroll-synchronized with the editor content.
- Readonly mode shows static highlighted code, without textarea.
- Copy button writes the full code string to the clipboard.
- All sizing and spacing scale automatically with the shared
sztoken. - Supports dark/light themes via existing design-token colors.
- Designed as a low-level editor component, not a full IDE replacement.
<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:"")
- Uses the new
HTMLInputElement.showPicker()API when available; falls back to focusing/clicking the hidden input. - Keeps the hidden color input in sync with controlled
valuethrough$effect. - Preview swatch mirrors the current color and announces via
aria-label. clearable=falsehides the clear button; whendisabled, 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>Right-click context menu for editor actions.
onUndo?: () => void- Fired when Undo is selectedonRedo?: () => void- Fired when Redo is selectedonCopy?: () => void- Fired when Copy is selectedonCut?: () => void- Fired when Cut is selectedonPaste?: () => void- Fired when Paste is selectedonDelete?: () => void- Fired when Delete is selected
- Call
openAt(event)from the parent to show the menu at the pointer. - Closes on outside click or
Escape. - Labels come from
lang-contextviasetContext("lang", { value }).
<script lang="ts">
import { setContext } from "svelte";
import ContextMenu from '$lib/ContextMenu.svelte';
setContext("lang", { value: "en" });
let menu: ContextMenu | null = null;
const onContext = (e: MouseEvent) => {
e.preventDefault();
menu?.openAt(e);
};
</script>
<div oncontextmenu={onContext}>
<p>Right click me</p>
<ContextMenu bind:this={menu} onCopy={() => console.log('copy')} />
</div>Button-driven date selector that formats the chosen value and supports min/max limits.
value?: string | null- Selected date value (ISOYYYY-MM-DD) (default:null)min?: string- Minimum selectable date (ISOYYYY-MM-DD)max?: string- Maximum selectable date (ISOYYYY-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:"")
- Uses
Calendar.sveltein a popover panel for a consistent in-app UI. - Formatter uses
Intl.DateTimeFormat(withlocale/formatOptions) and gracefully falls back totoLocaleDateString(). clearableresets the value and dispatchesonChange(null).- Preview card includes
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 content
- In modal mode the first interactive element is focused automatically and focus is trapped inside the dialog;
Escapetriggers cancel. onCloseruns afteronConfirm/onCancel, so you can centralize cleanup.- Non-modal mode (
modal=false) renders a floating panel without overlay or focus trap. - Buttons are labeled "OK" and "Cancel" and aren't customizable via props; provide
childrenfor full custom UI. - Set a meaningful
titlefor accessibility; it's used as the dialog'saria-label. szadjusts 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 whenas="input"invalid?: boolean- Marks the field invalid and setsaria-invalid(default:false)describedBy?: string- ID of helper or error text for accessibility
bind:valueis supported;onChangereceives cast value (numberwhenparseNumbersucceeds, otherwisestringor"").- Clear button appears only for text inputs (not for
type="number") and sets value to an empty string. - Automatic padding for leading/trailing content; label is linked via an auto-generated
id. - Accessibility: sets
aria-invalid,aria-describedby; number inputs also setinputmode="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)maxBytes?: number- Maximum allowed file size in bytesonError?: (error: string) => void- Fired when selected files are rejectedplaceholder?: string- Placeholder text for the drop zonevalue?: FileList | null- Controlled selected files (bindable) (default:null)onFilesSelected?: (files: FileList | null) => void- Fired when files are chosenclass?: string- Additional classes for the wrapper (default:"")
- The entire area is clickable and supports drag-and-drop.
- After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
acceptdoes not apply to dropped files, only to the picker UI; validate files insideonFilesSelected.- When
clearable=true, the user can clear selected files and the callback receivesnull. - When
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)
- Initial value for each field:
value[name]→schema.default→''(orfalsefor checkboxes). validateOn='input'|'blur'|'submit'controls when validators run; built-in checks:required,number, andemailregex.when(form)hides a field dynamically; hidden fields are skipped during validation.Selectoptions are coerced to strings for the underlying control; provide string values if you rely on strict equality.- Errors are rendered with stable
ids and wired viaaria-describedby;invalidflags are passed to inputs. exposeprovides{ reset, submit, validate, getData };validatereturnsPromise<boolean>.compactreduces 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)
- Clicking outside the panel or pressing
Escapecloses the drawer. - Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
- In controlled mode (
pressedis defined), state changes are requested viaonOpenChange(open). - When
menuItemsis empty, a “No items” placeholder is shown. - The drawer uses
role=\"dialog\"andaria-modal=\"true\"; the trigger reflects state viaaria-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)}
/>Install button that triggers the browser PWA prompt.
alwaysShow?: boolean- Forces the install button to be visible (default:false)inline?: boolean- Renders the button inline instead of fixed (default:false)class?: string- Additional button classes (default:"")
- Relies on the
beforeinstallpromptevent and HTTPS context. - The button only appears when the browser fires the event, unless
alwaysShowistrue.
<script lang="ts">
import InstallPWA from '$lib/InstallPWA.svelte';
</script>
<InstallPWA alwaysShow inline />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)
- Fully keyboard-safe for focus and mouse interactions.
- Submenus open on hover when another menu is already open.
- Actions that match size keys (
xs,sm,md,lg,xl) are automatically highlighted to reflect the current UI size. - Uses the same CSS variable architecture as Tabs for consistent look across components.
- No slots; fully controlled via the
menusstructure andonSelect.
<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}
/>Shared base used by Toast and Badge for consistent visuals.
title?: string- Optional title displayed above the messagemessage?: string- Notice text contentvariant?: ToastVariant- Visual style (success|danger|warning|info) (default:info)showIcon?: boolean- Shows an icon matching the variant (default:true)inline?: boolean- Inline layout without overlay styling (default:false)size?: "sm" | "md"- Size preset for spacing and typography (default:"sm")end?: Snippet- Trailing content (e.g. close button)class?: string- Additional wrapper classes (default:"")
- Intended as a low-level building block for notices.
- Used internally by
ToastandBadge.
<script lang="ts">
import NoticeBase from '$lib/NoticeBase.svelte';
</script>
<NoticeBase title="Saved" message="All changes stored" variant="success" />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- OptionalCardheader contentfooter?: Snippet- Custom footer content shown above paginationclass?: string- Extra classes passed to the underlyingCard(default:"")
- Maintains internal
currentPagestate (starts at1). totalPagesis clamped to at least1; emptyitemsstill yields one page.- Pagination is always visible; your
footersnippet renders before it. - Uses
Pagination.svelteinternally with{ currentPage, totalPages, onPageChange }. itemsPerPagemust 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:"")
- Displays “Page X of Y” and numbered page buttons.
- Prev/next buttons are disabled at the edges.
- Shows up to 3 numbered buttons centered around the current page.
- Uses
aria-current=\"page\"on the active page for accessibility. - Buttons are native
<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}
/>Theme primary-color selector built on top of Select. Provides a fixed palette,
sz?: SizeKey- Sizing preset passed directly to Select (xs|sm|md|lg|xl) (default:sm)label?: string- Custom label text. Falls back to localized copy when omitted.class?: string- Extra classes forwarded to the underlying Select component (default:"")
- The palette is predefined internally (
{ value, label, swatch }). - Selected value is stored in localStorage under "primary".
- The
htmlelement receivesdata-primary="{value}"for theme styling. - Uses the same onChange contract as Select and forwards palette options as-is.
<script lang="ts">
import PrimaryColorSelect from "$lib/PrimaryColorSelect.svelte"
</script>
<PrimaryColorSelect />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)
- Indeterminate animation for unknown progress.
- Auto-clamping between 0 and 100.
- Adaptive height based on size.
- Theming support via CSS variables.
- Fully accessible with proper
ariaroles. - No invalid ARIA attributes.
<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..." />Circular progress indicator for visualizing completion or load state (0-100). Supports indeterminate mode.
value?: number- Current progress value (default:0)indeterminate?: boolean- Enables spinning infinite mode (default:false)sz?: SizeKey- Size preset (xs|sm|md|lg|xl) (default:md)variant?: ComponentVariant- Color/style variant (default|neutral) (default:default)label?: string- Optional text shown above the circle (default:"")disabled?: boolean- Apply disabled styles (default:false)class?: string- Extra wrapper classes (default:"")
- Clamps value between 0-100
- Uses SVG stroke-dashoffset animation
- Accessible role=progressbar with aria-valuenow
- Works in both determinate/indeterminate modes
<script lang="ts">
import ProgressCircle from '$lib/ProgressCircle.svelte';
let value = 45;
</script>
<ProgressCircle value={value} label="Loading..." />
<ProgressCircle value={value} variant="neutral" disabled />
<ProgressCircle indeterminate label="..." />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")
- Fully supports native radio grouping through
bind:group - Works as a controlled component when
checkedandonChangeare used together childrentakes priority overlabel- Size and variant affect circle size, dot size and colors
- Hidden native input is focusable and exposes full accessibility attributes
- Uses data-state attributes to enable custom styling
<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>Search input field with a leading search icon.
label?: string- Label text rendered above the fieldplaceholder?: string- Placeholder text (localized by default)value?: string- Controlled field value (bindable) (default:"")sz?: SizeKey- Size preset for spacing and typography (xs|sm|md|lg|xl) (default:sm)variant?: FieldVariant- Visual style variant (default|filled|neutral) (default:filled)class?: string- Additional classes applied to the Field root (default:"")
- Renders a leading search icon and uses
Fieldwithtype="search"andclearable.
<script lang="ts">
import SearchInput from '$lib/SearchInput.svelte';
let query = '';
</script>
<SearchInput
label="Search"
placeholder="Type to search..."
bind:value={query}
variant="filled"
/>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 setsaria-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 scrolling
- Fully keyboard navigable (
Arrow,Home/End,Enter/Space, loopedTab) with roving tabindex buttons inside a listbox. - Dropdown is portalled with
position: fixedmath to stay aligned with the trigger (including scroll/resize listeners). - Hidden
<input type=\"hidden\">mirrorsbind:valueso forms and contextualBaseFieldintegrations keep working. - Disabled options remain focus-skippable and never call
onChange. - Size + variant tokens control both trigger padding and icon scale through the same Tailwind-driven design tokens.
<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:"")
- Works with both keyboard and mouse.
- Size and variant control appearance.
- Uses proper ARIA attributes.
<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
- Uses pointer events for smooth dragging with proper event delegation
- Responsive - automatically adjusts to container resize
- Accessible with proper cursor hints and hover states
- No wrapper elements - panels render directly for clean DOM structure
- Supports any content type through snippet rendering
<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:"")
- Built over a real checkbox so browser accessibility comes for free: keyboard (Space/Enter), focus ring, and screen reader semantics.
- Labels do not affect the actual checkbox hitbox, but the whole area is clickable if wrapped correctly.
- Size preset adjusts track width, knob size, and spacing.
- Reflects
disabledby dimming visuals and removing pointer events. - The component keeps no internal state besides the bound
checkedvalue, 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)
- Click a column header to toggle ascending or descending.
- In
denseandlistthe table uses content width (inline-table+w-fit). - Column
widthis applied only in non-compact variants (default,zebra,noBorder,noTitle). listhides the header row.- Integrates with
Pagination.sveltevia thepaginationprop. - Works in dark mode and supports keyboard sorting (Enter or Space on headers).
<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 withidandlabel(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 tabs
- Supports multiple visual styles (
default,pills,underline). - Fully accessible with keyboard navigation (Arrow keys, Home, End, Enter).
- Automatically adapts to container width.
- The panel content (
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")
- Uses
$effectto sync thedarkclass on<html>. - Default position is
fixed top-4 right-4, unless overridden by a customclass. - Styled entirely through CSS variables; fully theme-aware.
- Smooth transition between sun and moon icons.
<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 ISOHH: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:"")
- ISO mode uses 24-hour time; English mode uses 12-hour time with AM/PM
- The stored value is always ISO (
HH:MM) stepdefines the minute grid (derived from seconds)- No locale or date-formatting APIs are used internally
<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>Improved time picker implementation in src/lib/TimePickerNew.svelte. It keeps the stored value as ISO HH:MM, supports 24-hour and 12-hour display modes, includes Now/Clear/OK actions, and renders the selected-time preview inside the component.
value?: string | null- Stored time in ISOHH: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 clear action (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:"")
- The public stored value is always ISO
HH:MM, including when the UI is in 12-hour mode. - The popup is positioned with viewport clamping and updates on scroll/resize.
Nowsnaps minutes to the configuredstep.Clearresets the value tonull;OK, outside click andEscapeclose the popup.- Uses localized labels from
src/lib/lang.tsthrough the library lang context. - Uses CSS variables for colors, spacing, radius, shadows, typography and transitions.
<script lang="ts">
import TimePickerNew from '$lib/TimePickerNew.svelte';
let time: string | null = '13:30';
</script>
<TimePickerNew
label="Meeting time"
step={900}
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:"")
- Automatically hides after
timeout. - Can be closed manually if
closable = true. - Variant controls color and icon style.
<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:"")
- Wraps any inline element and shows a floating bubble with
text. openoverrides hover/focus behavior when set totrue.positioncontrols which side of the trigger the bubble appears on.delayadds a small pause before showing the tooltip to avoid flicker.classis 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>Responsive top navigation bar with optional hamburger menu and PWA install button.
title?: string- The title displayed in the center of the topbar (default:[])title?: string- The title displayed in the center of the topbar (default:[])onMenuSelect?: (id: string) => void- Callback when a menu item is selected (default:() => {})hamburgerHeader?: import("svelte").Snippet- Custom header content for the hamburger menu (default:undefined)hamburgerFooter?: import("svelte").Snippet- Custom footer content for the hamburger menu (default:undefined)showHamburger?: boolean- Whether to show the hamburger menu (default:false)children?: import("svelte").Snippet- Custom content to display in the center of the topbar (default:undefined)right?: import("svelte").Snippet- Custom content to display on the right side of the topbar (default:undefined)
- Fixed to the top of the viewport with a consistent height.
- Switches hamburger width and typography based on viewport size.
- Renders
InstallPWAinline on desktop and inside the hamburger header on mobile. - Designed to be used once at the page root.
<script lang="ts">
import Topbar from '$lib/Topbar.svelte';
const items = [
{ id: 'open', label: 'Open' },
{ id: 'save', label: 'Save' }
];
</script>
<Topbar title="FormBuilder" menuItems={items} showHamburger />- Built with Svelte 5 Runes (
$state,$derived,$effect,$props). - Styled with TailwindCSS and CSS variables; dark mode by
.dark. - Self-contained components; no global slots - content is passed via snippets using
{@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.