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 every one also 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 |
ContextMenu | Right-click menu anchored to the pointer, with arrow-key navigation | docs |
ConfirmDialog | Promise-based dialog queue, so two racing prompts canβt share one answer | docs |
DraggablePane | Floating panel you can drag by its header, resize and reset to its anchor | docs |
Sheet | Native modal edge panel with backdrop dismissal and focus restoration | docs |
Tabs | Controlled ARIA tabs with automatic or manual keyboard activation | docs |
Accordion | Single or multi-open disclosure group with snippet-rendered content | 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 |
CopyButton | Copy-to-clipboard button with pending, success and error states | 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> 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) => ({ 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>Replaces the browserβs right-click menu for a region. Takes the same actions as CommandMenu. ContextMenu docs β
svelte<script lang="ts">
import { ContextMenu } from 'svelte-widgets'
let last_run = $state(``)
const record = (label: string) => (last_run = label)
const actions = [
{ label: `Cut`, shortcut: `mod+x`, action: record },
{ label: `Copy`, shortcut: `mod+c`, action: record },
{ label: `Paste`, shortcut: `mod+v`, action: record },
]
</script>
<ContextMenu {actions}>
<div
style="display: grid; place-items: center; height: 6em; border: 1px dashed gray; border-radius: 5pt"
>
Right-click me
</div>
</ContextMenu>
{#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
// real routes so the prerender crawl doesn't trip over dead links
const routes = [`/`, `/multiselect`, `/popover`, `/toc`].map(resolve_path)
const link_props = { onclick: (event: MouseEvent) => event.preventDefault() }
</script>
<Nav {routes} {page} {link_props} />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.
$bindable props, so you can both read it and drive it from the outsidewindow or localStorage before mount| Statements | Branches | Lines |
|---|---|---|
npm install --dev 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 {
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
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 |
/clipboard | Clipboard feedback state |
/dialogs | Queued choice, confirmation and prompt requests |
/file-drop | Directory expansion and accept filtering |
/fullscreen | Shared fullscreen state |
/heading-anchors | Heading ID preprocessor, slugger and anchor attachment |
/icons | Dynamic icon registry |
/katex | KaTeX before/after preprocessor pair |
/live-examples | mdsvex live-example transform, Vite plugin and highlighter |
/live-examples/create-highlighter | Lightweight custom grammar highlighter factory |
/print | Element printing |
/text-search | Text ranges, highlighting and search-jump helpers |
/theme | Headless light/dark/system state |
/toast-queue | Toast reducer and reactive store |
/types | Shared component/action types |
/utils | Positioning, fuzzy matching, hotkeys and general helpers |
/vite-config | This repositoryβs Vite Plus configuration helper |
For $β¦$ and $$β¦$$ math in mdsvex, wrap mdsvex with katex_preprocess() and run heading_ids() last:
import { mdsvex } from 'mdsvex'
import { heading_ids } from 'svelte-widgets/heading-anchors'
import { katex_preprocess } from 'svelte-widgets/katex'
const katex = katex_preprocess()
export default {
preprocess: [katex.before, mdsvex({ extensions: [`.md`] }), katex.after, heading_ids()],
} Import katex/dist/katex.min.css once in the app so the generated markup is styled.
Popover and ContextMenu compose these three: a surface positioned by float,
dismissed by click_outside (on the press, so a right-click closes it too) and
keyboard-scoped by focus_trap.
<script lang="ts">
import { ContextMenu, Popover } from 'svelte-widgets'
</script>
<Popover placement="bottom" align="start">
{#snippet trigger(props)}
<button {...props}>Options</button>
{/snippet}
<p>Anything you like in here.</p>
</Popover>
<ContextMenu actions={[{ label: `Reload`, action: () => location.reload() }]}>
<div>Right-click anywhere in this region</div>
</ContextMenu> See src/lib/live-examples/readme.md for optional live-example helpers.
Here are some steps to get you started if youβd like to contribute to this project!