Svelte Widgets
Svelte Widgets

CI NPM version Needs Svelte version Playground Open in StackBlitz

Keyboard-friendly, accessible and highly customizable Svelte components. View the docs

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

ComponentWhat it doesDocs
MultiSelectKeyboard-friendly multi/single select with grouping, async loading and deep style hooksdocs
CommandMenuCommand palette with fuzzy search, hotkeys, recents and async actionsdocs
PageSearchPagefind-backed site search built on CommandMenudocs
PopoverFloating surface that positions, dismisses and traps focus for youdocs
ActionMenuAction list opened from a trigger or right-click, with complete menu keyboard semanticsdocs
ConfirmDialogPromise-based dialog queue, so two racing prompts can’t share one answerdocs
DialogNative modal with composable sections, close reasons and nested-dialog handlingdocs
DraggablePaneFloating panel you can drag by its header, resize and reset to its anchordocs
NumberRangeInputPaired number and range inputs with explicit min, max, and stepdocs
RangeSliderTwo-handle interval slider with numeric fields, step snapping, RTL, and keyboard controlsdocs
SplitPaneResizable panes with ratio or pixel bounds and collapse supportdocs
VirtualListFixed-height list virtualization with programmatic scrollingdocs
FileInputFile picker and drop zone with validation, cancellation and retrydocs
TreeViewKeyboard-navigable tree with lazy loading and custom node renderingdocs
JsonTreeSearchable JSON inspector with editing, copying and diffsdocs
ProgressAccessible determinate or indeterminate progressdocs
TaskStatusTask progress and errors with caller-owned cancellation and retrydocs
SettingsGroupCollapsible group for organizing related settings sectionsdocs
SettingsSearchSettings-row filter that expands matching groups and restores their prior statedocs
SettingsSectionTitled settings region with change tracking, resets, descriptions and shared-grid layoutdocs
SheetDialog-based modal edge panel with side placement and shared dismissal policiesdocs
TabsControlled ARIA tabs with automatic or manual keyboard activationdocs
AccordionSingle or multi-open disclosure group with snippet-rendered contentdocs
FindBarIn-DOM find-in-page bar that highlights, counts and steps through matchesdocs
CodeBlockRead-only code with cancellable highlighting, escaped tokens or trusted HTMLdocs
StatGridResponsive statistic tiles with units, hints and accessible changesdocs
SpinnerLoading status with optional textdocs
StatusMessageDismissible info, success, warning or error feedbackdocs
DragOverlayDrop-target overlay with an optional messagedocs
ClickFeedbackPositioned transient confirmation icondocs
CodeEditorVirtualized editable code surface with injectable highlighting and persistencedocs
DiffViewVirtualized side-by-side and unified diffs with an injectable backenddocs
ToastNotification queue with priorities, dedupe and pause-on-hoverdocs
NavNavigation bar with dropdowns, pinning and active-route stylingdocs
TocSticky table of contents that finds and tracks its own headingsdocs
MasonryColumn-balancing masonry grid with SSR support and virtualizationdocs
FooterCentered row of icon links, sized and themed with --footer-*docs
ActionButtonAsync action button with pending, success and error feedbackdocs
CopyButtonCopy-to-clipboard button with success and error feedbackdocs
ButtonGroupSegmented control over a set of options, single or multi selectdocs
FullscreenButtonFullscreen toggle scoped to one wrapper, so viewers don’t fight over the flagdocs
ThemeToggleLight/dark/system theme cycler with persistence and cross-tab synchronizationdocs
ToggleAccessible switch with a bindable checkeddocs
CodeExampleCollapsible source viewer used by the live examplesdocs
FileDetailsCollapsible <details> viewer for a set of filesdocs
PrevNextPrevious/next links for sequential pagesdocs
SubpageGridCard grid linking to child pagesdocs
IconInline SVG icon from the bundled setdocs
GitHubCornerThe classic corner ribbon linkdocs
CircleSpinnerMinimal loading spinnerdocs
ContributorListAvatar row of GitHub contributors, grayscale until hoverdocs
LiteYouTubeEmbedYouTube poster that only loads the player iframe once clickeddocs
WiggleSpring-animated shake wrapperdocs

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>

💡   Features

  • Lightweight components: core widgets need only Svelte; Markdown uses Marked and js-yaml, while math and syntax highlighting use optional peers
  • Keyboard friendly: every interactive component is fully operable without a mouse
  • Bindable: component state is exposed through $bindable props, so you can both read it and drive it from the outside
  • Themeable: CSS variables with sensible defaults on every element, plus prop bags to spread arbitrary attributes onto internals
  • SSR-safe: nothing touches window or localStorage before mount
  • Typed: props, snippets and events are inferred from the data you pass

🧪   Coverage

The unit CI job reports current coverage and enforces the thresholds in vite.config.ts.

🔨   Installation

npm install -D svelte-widgets

🚚   Migrating from svelte-multiselect

This 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'.

📦   Subpath exports

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'
SubpathAPI
/attachmentsElement attachments and dismissal primitives
/canvasParent content-box sizing, DPR tracking and coalesced canvas redraws
/csvCSV escaping and row serialization with optional explicit columns
/formatBinary byte-size formatting
/roving-focusOne keyboard tab stop across available HTML or SVG items, including nested groups
/statsStatistic value and change formatting
/url-paramsTyped query validation and URL updates that omit defaults
/clipboardClipboard feedback state
/code-editorBackend-agnostic editing, diff rendering and primitives
/code-editor/editor.cssShared syntax-token and diff-view styles
/dialogsQueued choice, confirmation and prompt requests
/file-dropDirectory expansion and accept filtering
/find-in-pageReactive find-in-page cursor behind FindBar
/fullscreenShared fullscreen state
/heading-anchorsHeading ID preprocessor, slugger and anchor attachment
/image-markupImage-fit geometry and canvas rendering of freehand annotation strokes
/iconsDynamic icon registry
/json-treeJSON inspector component and types
/json-tree/pathDot/bracket path formatting and resolution
/json-tree/utilsJSON traversal, immutable path edits, search and diff helpers
/labelsDefault UI strings for i18n, incl. attachments & helpers
/highlightLazy default and custom grammar highlighters
/markdownMarkdown-to-Svelte preprocessor and direct HTML renderer
/markdown/viteLive code examples with virtual modules and hot reload
/markdown/contentContent manifests, typed frontmatter, link validation, TOC and search records
/markdown/checkNode-only syntax, type and assertion checks for documentation examples
/printPage printing with a suggested PDF filename
/source-linksLink inline code mentions of your source to GitHub
/source-links/vite-pluginVite plugin emitting the file/export index those links use
/source-links/virtualTypes for the plugin’s virtual:source-symbols module
/storageNon-throwing localStorage, persisted choices and MRU lists
/text-searchText ranges, highlighting and search-jump helpers
/themeHeadless light/dark/system state
/toast-queueToast reducer and reactive store
/utilsPositioning, fuzzy matching, hotkeys and general helpers
/virtualVisible-window calculation for fixed-size items
/vite-configThis 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.

🆕   Changelog

View the changelog.

🙏   Contributing

Here are some steps to get you started if you’d like to contribute to this project!

📚   Demos

🚀   Getting Started

One live example per component. Each links to its full page.

MultiSelect

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>

CommandMenu

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}

Popover

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>

ActionMenu

Shows the same action list from a button trigger or a right-click region. Takes the same actions as CommandMenu. ActionMenu docs →

Right-click me
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} />

Toc

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 →

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.

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>

Masonry

Balances items across as many columns as the container fits, measuring each one so uneven heights pack tightly. Masonry docs →

1
2
3
4
5
6
7
8
9
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>

CopyButton

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

The rest

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.