Tabs

Tabs keeps selection bindable and supports automatic or manual keyboard activation. It intentionally ships no visual chrome; style .tabs-list, .tabs-tab, .tabs-panel and their data-state attributes.

Overview panel. Current value: overview.

svelte<script lang="ts">
  import { Tabs } from 'svelte-widgets'

  const items = [
    { value: `overview`, label: `Overview` },
    { value: `api`, label: `API` },
    { value: `disabled`, label: `Disabled`, disabled: true },
  ] as const
  let value = $state<(typeof items)[number][`value`]>(`overview`)
</script>

<Tabs {items} bind:value activation="manual" label="Package sections">
  {#snippet panel({ item })}
    <p>{item.label} panel. Current value: <code>{value}</code>.</p>
  {/snippet}
</Tabs>

Accordion

Accordion uses real heading buttons and regions. Set mode="multiple" to bind an array of open values, or set collapsible={false} in single mode to keep the open item from closing itself. It is also headless: .accordion-item, .accordion-trigger, .accordion-panel and data-state are the styling hooks.

Content for Installation.

svelte<script lang="ts">
  import { Accordion } from 'svelte-widgets'

  const items = [
    { value: `install`, label: `Installation` },
    { value: `usage`, label: `Usage` },
    { value: `disabled`, label: `Unavailable`, disabled: true },
  ] as const
  let value = $state<string[]>([`install`])
</script>

<Accordion {items} mode="multiple" bind:value>
  {#snippet panel({ item })}
    <p>Content for {item.label}.</p>
  {/snippet}
</Accordion>

FindBar

FindBar is an in-DOM find-in-page bar over search_text: it highlights every match under root, counts them, and walks them with Enter / Shift+Enter. Matches wear a CSS Custom Highlight, which is document-global and so cannot be styled from inside the component — add your own ::highlight(find-match) rule. Where the chrome does not fit, create_find_state from svelte-widgets/find-in-page gives you the same cursor with no markup.

Because this example renders the bar inside root, also_ignore=".find-bar" keeps its status text and controls out of the searchable section content.

Ordered subsequence matching, whitespace collapsing and cross-node matches all come from search_text, so a query can straddle inline markup.

Collapsed sections open on a jump

Just like the browser's own find-in-page.

svelte<script lang="ts">
  import { FindBar } from 'svelte-widgets'

  let root = $state<HTMLElement>()
  let open = $state(true)
</script>

<div bind:this={root} style="position: relative; padding-top: 3rem">
  {#if open}
    <FindBar
      {root}
      label="section"
      also_ignore=".find-bar"
      on_close={() => (open = false)}
    />
  {:else}
    <button onclick={() => (open = true)}>Find in section</button>
  {/if}
  <p>
    Ordered subsequence matching, whitespace collapsing and cross-node matches all come
    from <code>search_text</code>, so a query can straddle inline markup.
  </p>
  <details>
    <summary>Collapsed sections open on a jump</summary>
    <p>Just like the browser's own find-in-page.</p>
  </details>
</div>

<style>
  :global(::highlight(find-match)) {
    background: rgba(110, 168, 255, 0.35);
  }
</style>

Dialog

Dialog is a centered native modal with bindable state and surface, composable trigger/header/footer snippets, focus restoration and nested-dialog stacking. on_close reports pointer, escape or close for dismissals started inside the dialog; it does not fire when you set the bound open state to false yourself. Use closedby="any" (the default) for backdrop and Escape dismissal, closedby="closerequest" for Escape only, or closedby="none" to require an explicit close action. Keep nested dialogs and other overlays inside its content so the browser’s modal top layer can make only the innermost one interactive.

Last close: none

svelte<script lang="ts">
  import { Dialog, type DialogProps } from 'svelte-widgets'

  let open = $state(false)
  let last_close = $state(`none`)
  let closedby = $state<DialogProps[`closedby`]>(`any`)
</script>

<label
  >Dismissal policy
  <select bind:value={closedby}>
    <option value="any">Backdrop and Escape</option>
    <option value="closerequest">Escape only</option>
    <option value="none">Explicit close only</option>
  </select>
</label>
<Dialog
  bind:open
  {closedby}
  aria-labelledby="profile-dialog-title"
  on_close={({ via }) => (last_close = via)}
>
  {#snippet trigger(props)}
    <button {...props}>Edit profile</button>
  {/snippet}
  {#snippet header({ close })}
    <h2 id="profile-dialog-title" style="margin: 0">Edit profile</h2>
    <button onclick={close}>Close</button>
  {/snippet}
  <label>Display name <input value="Ada" /></label>
  <Dialog aria-label="Advanced profile settings">
    {#snippet trigger(props)}
      <button {...props}>Advanced</button>
    {/snippet}
    Nested modal content
  </Dialog>
  {#snippet footer({ close })}
    <button onclick={close}>Save</button>
  {/snippet}
</Dialog>

<p>Last close: <code>{last_close}</code></p>

Sheet

Sheet composes Dialog as a side-positioned variant, so it shares the same snippets, close policies, focus restoration and nested-modal behavior. Keep overlays inside its dialog; portalling them to body moves them outside the modal subtree and makes them inert.

svelte<script lang="ts">
  import { Sheet } from 'svelte-widgets'

  let open = $state(false)
</script>

<Sheet bind:open side="right" aria-label="Settings">
  {#snippet trigger(props)}
    <button {...props}>Open settings</button>
  {/snippet}
  {#snippet header({ close })}
    <strong>Settings</strong>
    <button onclick={close}>Close</button>
  {/snippet}
  <label>Project name <input value="Svelte Widgets" /></label>
  {#snippet footer({ close })}
    <button onclick={close}>Done</button>
  {/snippet}
</Sheet>