Getting started
Scoped with headingSelector so it ignores the rest of the page.
Configuration
Pass collapseSubheadings to fold levels under their parent.
Troubleshooting
Set warnOnEmpty to hear about a selector that matches nothing.
Keyboard-friendly, accessible and highly customizable Svelte components.
Every component is a named export from the package root and has a direct subpath import (svelte-widgets/Toc.svelte) so bundlers can skip the rest.
| Component | What it does | Docs |
|---|---|---|
MultiSelect | Keyboard-friendly multi/single select with grouping, async loading and deep style hooks | docs |
CommandMenu | Command palette with fuzzy search, hotkeys, recents and async actions | docs |
PageSearch | Pagefind-backed site search built on CommandMenu | docs |
Popover | Floating surface that positions, dismisses and traps focus for you | docs |
ActionMenu | Action list opened from a trigger or right-click, with complete menu keyboard semantics | docs |
ConfirmDialog | Promise-based dialog queue, so two racing prompts can’t share one answer | docs |
Dialog | Native modal with composable sections, close reasons and nested-dialog handling | docs |
DraggablePane | Floating panel you can drag by its header, resize and reset to its anchor | docs |
NumberRangeInput | Paired number and range inputs with explicit min, max, and step | docs |
RangeSlider | Two-handle interval slider with numeric fields, step snapping, RTL, and keyboard controls | docs |
SplitPane | Resizable panes with ratio or pixel bounds and collapse support | docs |
VirtualList | Fixed-height list virtualization with programmatic scrolling | docs |
FileInput | File picker and drop zone with validation, cancellation and retry | docs |
TreeView | Keyboard-navigable tree with lazy loading and custom node rendering | docs |
JsonTree | Searchable JSON inspector with editing, copying and diffs | docs |
Progress | Accessible determinate or indeterminate progress | docs |
TaskStatus | Task progress and errors with caller-owned cancellation and retry | docs |
SettingsGroup | Collapsible group for organizing related settings sections | docs |
SettingsSearch | Settings-row filter that expands matching groups and restores their prior state | docs |
SettingsSection | Titled settings region with change tracking, resets, descriptions and shared-grid layout | docs |
Sheet | Dialog-based modal edge panel with side placement and shared dismissal policies | docs |
Tabs | Controlled ARIA tabs with automatic or manual keyboard activation | docs |
Accordion | Single or multi-open disclosure group with snippet-rendered content | docs |
FindBar | In-DOM find-in-page bar that highlights, counts and steps through matches | docs |
CodeBlock | Read-only code with cancellable highlighting, escaped tokens or trusted HTML | docs |
StatGrid | Responsive statistic tiles with units, hints and accessible changes | docs |
Spinner | Loading status with optional text | docs |
StatusMessage | Dismissible info, success, warning or error feedback | docs |
DragOverlay | Drop-target overlay with an optional message | docs |
ClickFeedback | Positioned transient confirmation icon | docs |
CodeEditor | Virtualized editable code surface with injectable highlighting and persistence | docs |
DiffView | Virtualized side-by-side and unified diffs with an injectable backend | docs |
Toast | Notification queue with priorities, dedupe and pause-on-hover | docs |
Nav | Navigation bar with dropdowns, pinning and active-route styling | docs |
Toc | Sticky table of contents that finds and tracks its own headings | docs |
Masonry | Column-balancing masonry grid with SSR support and virtualization | docs |
Footer | Centered row of icon links, sized and themed with --footer-* | docs |
ActionButton | Async action button with pending, success and error feedback | docs |
CopyButton | Copy-to-clipboard button with success and error feedback | docs |
ButtonGroup | Segmented control over a set of options, single or multi select | docs |
FullscreenButton | Fullscreen toggle scoped to one wrapper, so viewers don’t fight over the flag | docs |
ThemeToggle | Light/dark/system theme cycler with persistence and cross-tab synchronization | docs |
Toggle | Accessible switch with a bindable checked | docs |
CodeExample | Collapsible source viewer used by the live examples | docs |
FileDetails | Collapsible <details> viewer for a set of files | docs |
PrevNext | Previous/next links for sequential pages | docs |
SubpageGrid | Card grid linking to child pages | docs |
Icon | Inline SVG icon from the bundled set | docs |
GitHubCorner | The classic corner ribbon link | docs |
CircleSpinner | Minimal loading spinner | docs |
ContributorList | Avatar row of GitHub contributors, grayscale until hover | docs |
LiteYouTubeEmbed | YouTube poster that only loads the player iframe once clicked | docs |
Wiggle | Spring-animated shake wrapper | docs |
Fifteen attachments work on any element: fourteen come from svelte-widgets/attachments, while heading_anchors has its own subpath. dismiss_on_outside_press is the lower-level multi-surface primitive behind click_outside.
<script>
import { CommandMenu, MultiSelect, Popover, Tabs, Toc } from 'svelte-widgets'
</script>
$bindable props, so you can both read it and drive it from the outsidewindow or localStorage before mountThe unit CI job reports current coverage and enforces the thresholds in vite.config.ts.
npm install -D svelte-widgets
svelte-multiselectThis package was called svelte-multiselect up to v11 (#432). Swap it out:
npm uninstall svelte-multiselect && npm install -D svelte-widgets
Then rewrite the imports. Matching on the opening quote (all three kinds) keeps prose and GitHub URLs untouched, and covers every subpath along with the bare import. It skips .md deliberately: in markdown a backtick-quoted mention is usually prose, not an import.
find src -type f \( -name '*.svelte' -o -name '*.ts' -o -name '*.js' \) -exec perl -pi -e "s{(['\"\`])svelte-multiselect}{\$1svelte-widgets}g" {} +
Three things the rewrite cannot do for you: CmdPalette is now CommandMenu and PagefindPalette is now PageSearch (#428), and click_outside changed shape (it dismisses on pointerdown, and exclude/include merged into one inside option) (#431). See the changelog for the details.
Coming from svelte-toc or svelte-bricks instead? Those are now Toc and Masonry here (#432), so the same swap applies with import { Toc } from 'svelte-widgets' and import { Masonry } from 'svelte-widgets'.
Components have direct .svelte entry points, and headless/build-time APIs have focused subpaths:
import {
auto_update_position, // coalesce floating-position updates and clean up listeners
click_outside, // dismiss a surface when a press lands outside it
draggable,
float, // park an element next to an anchor and keep it there
focus_trap, // keep Tab inside a surface, hand focus back when it closes
highlight_matches,
hotkey, // declarative keybindings, `mod` maps to Cmd or Ctrl
register_escape_layer, // add a handler to the shared LIFO Escape stack
sortable,
tooltip,
} from 'svelte-widgets/attachments'
import { compute_position, fuzzy_match, get_label } from 'svelte-widgets/utils'
import { heading_anchors } from 'svelte-widgets/heading-anchors'
| Subpath | API |
|---|---|
/attachments | Element attachments and dismissal primitives |
/canvas | Parent content-box sizing, DPR tracking and coalesced canvas redraws |
/csv | CSV escaping and row serialization with optional explicit columns |
/format | Binary byte-size formatting |
/roving-focus | One keyboard tab stop across available HTML or SVG items, including nested groups |
/stats | Statistic value and change formatting |
/url-params | Typed query validation and URL updates that omit defaults |
/clipboard | Clipboard feedback state |
/code-editor | Backend-agnostic editing, diff rendering and primitives |
/code-editor/editor.css | Shared syntax-token and diff-view styles |
/dialogs | Queued choice, confirmation and prompt requests |
/file-drop | Directory expansion and accept filtering |
/find-in-page | Reactive find-in-page cursor behind FindBar |
/fullscreen | Shared fullscreen state |
/heading-anchors | Heading ID preprocessor, slugger and anchor attachment |
/image-markup | Image-fit geometry and canvas rendering of freehand annotation strokes |
/icons | Dynamic icon registry |
/json-tree | JSON inspector component and types |
/json-tree/path | Dot/bracket path formatting and resolution |
/json-tree/utils | JSON traversal, immutable path edits, search and diff helpers |
/labels | Default UI strings for i18n, incl. attachments & helpers |
/highlight | Lazy default and custom grammar highlighters |
/markdown | Markdown-to-Svelte preprocessor and direct HTML renderer |
/markdown/vite | Live code examples with virtual modules and hot reload |
/markdown/content | Content manifests, typed frontmatter, link validation, TOC and search records |
/markdown/check | Node-only syntax, type and assertion checks for documentation examples |
/print | Page printing with a suggested PDF filename |
/source-links | Link inline code mentions of your source to GitHub |
/source-links/vite-plugin | Vite plugin emitting the file/export index those links use |
/source-links/virtual | Types for the plugin’s virtual:source-symbols module |
/storage | Non-throwing localStorage, persisted choices and MRU lists |
/text-search | Text ranges, highlighting and search-jump helpers |
/theme | Headless light/dark/system state |
/toast-queue | Toast reducer and reactive store |
/utils | Positioning, fuzzy matching, hotkeys and general helpers |
/virtual | Visible-window calculation for fixed-size items |
/vite-config | This repository’s Vite Plus configuration helper |
create_canvas_surface() owns both layers’ inline CSS dimensions and restores them on cleanup. Supply height() or give the parent a definite height; draw callbacks receive CSS-pixel coordinates and isolated context state. create_roving_focus() keeps nested groups independent and observes DOM eligibility changes, including hidden panels and disabled items.
StatGrid changes are neutral by default; set an item’s delta_tone to positive or negative when the change has that meaning. ClickFeedback restarts when given a fresh position object, even at identical coordinates. rows_to_csv(rows, columns) accepts explicit readonly columns for sparse rows or header-only exports. URL validators accept native Sets or record keys; present empty strings remain valid when allowed.
CodeEditor and DiffView take host-supplied EditorBackend and DiffBackend implementations, either through their backend props or once per app with set_editor_backend() and set_diff_backend(). Import svelte-widgets/code-editor/editor.css alongside them for the token palette and shared line metrics. The editor takes a host-owned model={create_editor_model({ uri, text })} whose rope, UTF-16 selection, transactions, dirty checkpoint, and bounded history remain usable at 100 MB / 1,000,000 lines. Saving is an optional callback, so file reads, persistence, conflicts and draft policy remain in the host. The editable DOM temporarily remains a full-document textarea and is therefore still subject to browser textarea and scroll-height limits. Both backend contracts are runtime-agnostic and can call a native process, worker, WASM module or server route.
Run the opt-in, hardware-sensitive editor stress target locally with RUN_LARGE_EDITOR_TESTS=1 npx vitest run tests/vitest/code-editor-model.test.ts; normal CI deliberately skips it.
Use markdown() for Markdown pages with YAML frontmatter, embedded Svelte, GFM tables and task lists. Enable math for KaTeX. Markdown assigns its own heading IDs; use heading_ids() for native Svelte pages:
import { create_markdown, markdown } from 'svelte-widgets/markdown'
import { heading_ids } from 'svelte-widgets/heading-anchors'
export default {
extensions: [`.svelte`, `.md`],
preprocess: [markdown(create_markdown({ math: true })), heading_ids()],
}
Parse once with engine.parse(source, { dialect: "markdown" }), then pass the document to render_markdown() for HTML strings. Access frontmatter as metadata.title; fence settings are validated during parsing. Use check_document(document, options) from /markdown/check for one-shot documentation checks. Use assert_ok() to unwrap results at build boundaries and markdown_vite(engine) for runnable code fences. See the Markdown API for configuration and migration details. Import katex/dist/katex.min.css once when enabling math.
Popover and ActionMenu use the browser Popover API for top-layer rendering, light dismissal and Escape handling, while float supplies placement. Explicit custom dismissal policies still use click_outside. Dialog-like popovers can add focus_trap; action menus use Arrow/Home/End navigation and close on Tab so browser focus continues in page order.
<script lang="ts">
import { ActionMenu, Popover } from 'svelte-widgets'
const actions = [{ label: `Reload`, action: () => location.reload() }]
</script>
<Popover placement="bottom" align="start">
{#snippet trigger(props)}
<button {...props}>Options</button>
{/snippet}
<p>Anything you like in here.</p>
</Popover>
<ActionMenu {actions}>
{#snippet trigger(props)}
<button {...props}>Page actions</button>
{/snippet}
</ActionMenu>
<ActionMenu {actions}>
<div>Right-click anywhere in this region</div>
</ActionMenu>
See the Markdown guide for highlighting and runnable examples.
Docs that mention source files or exports in inline code (`Footer`, `make_config`) can link them to the GitHub line they live on, pinned to the commit the site was built from. Add the plugin to vite.config.ts, reference its virtual-module types from src/app.d.ts and attach the linker to the element that wraps your pages:
// vite.config.ts
import source_links from 'svelte-widgets/source-links/vite-plugin'
export default { plugins: [sveltekit(), source_links()] } // indexes src/lib by default
// src/app.d.ts
/// <reference types="svelte-widgets/source-links/virtual" />
// src/site/source-links.ts
import { create_source_links } from 'svelte-widgets/source-links'
import * as source_symbols from 'virtual:source-symbols'
export const { link_source_mentions, source_href } = create_source_links(source_symbols)
<main {@attach link_source_mentions}>{@render children()}</main>
Only exact, unambiguous names link: a file name or bare component name (Footer, utils.ts) points at the file, an exported definition (make_config) at its line, and names defined in several files (index.ts) or that aren’t source (label) are left alone. source_href(name) gives the same URL for use in your own markup.
Here are some steps to get you started if you’d like to contribute to this project!
One live example per component. Each links to its full page.
Type to filter, click or arrow-key to pick. selected is bindable in both directions. MultiSelect docs →
You selected: []
svelte<script lang="ts">
import { MultiSelect } from 'svelte-widgets'
const fruits: string[] = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']
let selected: string[] = $state([])
</script>
<MultiSelect id="fruits" bind:selected options={fruits} placeholder="Choose fruits..." />
<p>You selected: {JSON.stringify(selected)}</p>A command palette with fuzzy search over your actions. triggers binds it to a modifier
chord, or drive open yourself. CommandMenu docs →
svelte<script lang="ts">
import { CommandMenu } from 'svelte-widgets'
let open = $state(false)
let last_run = $state(``)
const actions = [`Toggle theme`, `Copy link`, `Open settings`, `Sign out`].map(
(label) => ({ id: label, label, action: () => (last_run = label) }),
)
</script>
<button onclick={() => (open = true)}>Open command menu</button>
<CommandMenu {actions} bind:open triggers={[]} />
{#if last_run}<p>ran: <code>{last_run}</code></p>{/if}A floating surface that positions itself where it fits, traps Tab and closes on Escape or an outside press. Popover docs →
svelte<script lang="ts">
import { Popover } from 'svelte-widgets'
</script>
<Popover placement="bottom" align="start">
{#snippet trigger(props)}
<button {...props}>Open popover</button>
{/snippet}
<p style="margin: 0 0 6pt">Tab is trapped in here.</p>
<label>Name <input placeholder="type something" /></label>
</Popover>Shows the same action list from a button trigger or a right-click region. Takes the same
actions as CommandMenu. ActionMenu docs →
svelte<script lang="ts">
import { ActionMenu } from 'svelte-widgets'
let last_run = $state(``)
const record = (label: string) => (last_run = label)
const actions = [
{ id: `Cut`, label: `Cut`, shortcut: `mod+x`, action: record },
{ id: `Copy`, label: `Copy`, shortcut: `mod+c`, action: record },
{ id: `Paste`, label: `Paste`, shortcut: `mod+v`, action: record },
]
</script>
<ActionMenu {actions}>
{#snippet trigger(props)}
<button {...props}>Actions</button>
{/snippet}
</ActionMenu>
<ActionMenu {actions}>
<div
style="display: grid; place-items: center; height: 6em; border: 1px dashed gray; border-radius: 5pt"
>
Right-click me
</div>
</ActionMenu>
{#if last_run}<p>ran: <code>{last_run}</code></p>{/if}A navigation bar with dropdowns, active-route styling and a mobile burger menu. Nav docs →
svelte<script lang="ts">
import { resolve } from '$app/paths'
import { page } from '$app/state'
import { Nav } from 'svelte-widgets'
const resolve_path = resolve as (path: string) => string
// Explicit labels stay identical when resolve() returns relative paths during SSR.
const routes = [
[`/`, `Home`],
[`/multiselect`, `MultiSelect`],
[`/popover`, `Popover`],
[`/toc`, `Toc`],
].map(([path, label]) => ({ href: resolve_path(path), label }))
const link_props = { onclick: (event: MouseEvent) => event.preventDefault() }
</script>
<!-- breakpoint={0} keeps this inline on phones: the mobile burger is position: fixed, so an
embedded demo would pin a second one over the site's own nav in the same corner -->
<Nav {routes} {page} {link_props} breakpoint={0} />Finds the headings itself, watches for late-rendered ones and tracks which is in view. The
one on the right of this page is a Toc. Toc docs →
Scoped with headingSelector so it ignores the rest of the page.
Pass collapseSubheadings to fold levels under their parent.
Set warnOnEmpty to hear about a selector that matches nothing.
svelte<script lang="ts">
import { Toc } from 'svelte-widgets'
</script>
<div class="toc-demo" style="display: flex; gap: 2em">
<article style="flex: 1">
<h3 id="toc-demo-getting-started">Getting started</h3>
<p>Scoped with <code>headingSelector</code> so it ignores the rest of the page.</p>
<h3>Configuration</h3>
<p>Pass <code>collapseSubheadings</code> to fold levels under their parent.</p>
<h3>Troubleshooting</h3>
<p>Set <code>warnOnEmpty</code> to hear about a selector that matches nothing.</p>
</article>
<Toc
headingSelector=".toc-demo h3"
breakpoint={0}
title="On this page"
style="position: static; width: 12em"
/>
</div>Balances items across as many columns as the container fits, measuring each one so uneven heights pack tightly. Masonry docs →
svelte<script lang="ts">
import { Masonry } from 'svelte-widgets'
// deterministic pseudo-random heights so the packing is visible but stable
const items = Array.from({ length: 9 }, (_, idx) => ({
id: idx,
height: 40 + ((idx * 37) % 90),
}))
</script>
<Masonry {items} minColWidth={120} gap={10}>
{#snippet children({ item })}
<div
style="height: {item.height}px; display: grid; place-items: center; border-radius: 4pt; background: var(--surface)"
>
{item.id + 1}
</div>
{/snippet}
</Masonry>Copies its content and cycles through success and error states. Every code block on this site has one. CopyButton docs →
svelte<script lang="ts">
import { CopyButton } from 'svelte-widgets'
</script>
<CopyButton content="npm install -D svelte-widgets" />ThemeToggle, Toggle, Icon, CircleSpinner, FileDetails, PrevNext, SubpageGrid, GitHubCorner and CodeExample are demoed together on the extras page, and the ten attachments have their own page.