Svelte Widgets
Svelte Widgets

Tests GitHub Pages 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 every one also 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
ContextMenuRight-click menu anchored to the pointer, with arrow-key navigationdocs
ConfirmDialogPromise-based dialog queue, so two racing prompts can’t share one answerdocs
DraggablePaneFloating panel you can drag by its header, resize and reset to its anchordocs
SheetNative modal edge panel with backdrop dismissal and focus restorationdocs
TabsControlled ARIA tabs with automatic or manual keyboard activationdocs
AccordionSingle or multi-open disclosure group with snippet-rendered contentdocs
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
CopyButtonCopy-to-clipboard button with pending, success and error statesdocs
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>

πŸ“š   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) => ({ 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>

ContextMenu

Replaces the browser’s right-click menu for a region. Takes the same actions as CommandMenu. ContextMenu docs β†’

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

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.

πŸ’‘   Features

  • No run-time deps: every component needs only Svelte as a peer dependency
  • 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

StatementsBranchesLines
StatementsBranchesLines

πŸ”¨   Installation

npm install --dev 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 {
  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'
SubpathAPI
/attachmentsElement attachments and dismissal primitives
/clipboardClipboard feedback state
/dialogsQueued choice, confirmation and prompt requests
/file-dropDirectory expansion and accept filtering
/fullscreenShared fullscreen state
/heading-anchorsHeading ID preprocessor, slugger and anchor attachment
/iconsDynamic icon registry
/katexKaTeX before/after preprocessor pair
/live-examplesmdsvex live-example transform, Vite plugin and highlighter
/live-examples/create-highlighterLightweight custom grammar highlighter factory
/printElement printing
/text-searchText ranges, highlighting and search-jump helpers
/themeHeadless light/dark/system state
/toast-queueToast reducer and reactive store
/typesShared component/action types
/utilsPositioning, fuzzy matching, hotkeys and general helpers
/vite-configThis 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.

πŸ†•   Changelog

View the changelog.

πŸ™   Contributing

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