svelte-widgets

repository·main·Indexed 18 days ago

https://github.com/janosh/svelte-multiselect

A collection of accessible, keyboard-friendly, and highly customizable Svelte components and attachments. Version 1.4.0 includes UI patterns such as CommandMenu, MultiSelect, Popover, Tabs, and Toc, as well as utility subpaths for element attachments, theme management, and code editor primitives. The library features a centralized dialog queuing system for sequential in-app prompts and is SSR-safe with no run-time dependencies other than Svelte.

Tokens
56.5K
Snippets
165
Records
186
Agent score
64%

What's inside svelte-widgets

  1. Core features of svelte-widgets

    main

    The svelte-widgets library provides a suite of accessible, keyboard-friendly, and highly customizable components with the following characteristics:

    • No run-time deps: Requires only Svelte as a peer dependency.
    • Keyboard friendly: All interactive components are fully operable without a mouse.
    • Bindable: Component state is exposed via $bindable props for two-way data binding.
    • Themeable: Uses CSS variables for easy styling, with prop bags available to spread attributes onto internal elements.
    • SSR-safe: Does not access window or localStorage before the component mounts.
    • Typed: Props, snippets, and events are inferred from the provided data.
  2. Implement cursor-based pagination in loadOptions

    main

    If your API uses opaque cursors (like Stripe or GitHub GraphQL) instead of offset/limit, you can manage the cursor within a closure.

    Important: To prevent the component from resetting the loaded batch, ensure you pass a stable function reference to loadOptions. When offset === 0, it indicates a fresh search or a reopened dropdown, and you should reset your cursor.

    function make_load_options() {
      let cursor: string | null = null
    
      return async ({ search, offset, limit, signal }: LoadOptionsParams) => {
        // Reset cursor on new search or dropdown open
        if (offset === 0) cursor = null
    
        const params = new URLSearchParams({ q: search, limit: `${limit}` })
        if (cursor) params.set(`cursor`, cursor)
    
        const response = await fetch(`/api/items?${params}`, { signal })
        const { items, next_cursor } = await response.json()
        
        cursor = next_cursor
        return { options: items, hasMore: Boolean(next_cursor) }
      }
    }
    
    // In your Svelte component:
    const load_options = make_load_options()
    // Pass the stable reference
    <MultiSelect loadOptions={load_options} />
  3. Enable and configure selection history (Undo/Redo)

    main

    The MultiSelect component includes built-in selection history by default, allowing users to undo and redo changes.

    • Enable/Disable: Use the history prop.
      • true (default): Enables history with a maximum of 50 entries.
      • number: Sets a custom maximum number of history entries.
      • false or 0: Disables history entirely.
    • Keyboard Shortcuts: By default, users can use Ctrl+Z / Cmd+Z to undo and Ctrl+Shift+Z / Cmd+Shift+Z to redo (platform-aware). You can override these using the shortcuts prop.
    • Behavior:
      • History tracks all changes to the selected array, including updates made via props from outside the component.
      • Performing a new selection after an undo operation will clear the redo stack.
      • Undo/redo functionality is automatically disabled when the component is in a disabled state.
    <MultiSelect history={50} />
    <!-- or to disable -->
    <MultiSelect history={false} />
    <!-- or to customize shortcuts -->
    <MultiSelect history={true} shortcuts={{ undo: 'alt+z', redo: 'alt+shift+z' }} />
  4. Mental model for MultiSelect props

    main

    Understanding the core props of MultiSelect helps in managing selection state:

    PropPurposeValue
    optionsWhat users can choose fromArray of strings, numbers, or objects with a label property
    bind:selectedWhich options users have chosenAlways an array: [], ['Apple'] or ['Apple', 'Banana']
    bind:valueSingle-select convenience for the user-selected optionSingle item: 'Apple' (or null) if maxSelect={1}, otherwise same as selected
  5. How FullscreenButton and sync_fullscreen manage state

    main

    The FullscreenButton component and the headless sync_fullscreen function maintain a two-way synchronization between a bindable fullscreen flag and the browser's actual fullscreen state. The fullscreen flag acts as the single source of truth: whether the user clicks the button, manually flips the flag in code, or presses <kbd>Esc</kbd>, the flag and the browser state will stay in agreement.

    Key Behavior:

    • Isolation: Synchronization is keyed to a specific wrapper element. If you have multiple independent wrappers on a page, each with its own flag, toggling fullscreen for one will not affect the flags of the others. This prevents the issue where every component on a page tries to trigger requestFullscreen simultaneously.
    • Background Painting: To prevent a black screen when entering fullscreen, the library automatically paints the page background onto a CSS variable. By default, this is --fullscreen-bg, but you can customize it using the bg_css_var option.
    <FullscreenButton 
      wrapper={wrapper_element} 
      bind:fullscreen={fullscreen_flag} 
      bg_css_var="--custom-bg-var" 
    />
  6. Configure Masonry item ordering

    main

    The order prop determines how items are placed into columns. It accepts values of type MasonryOrder:

    • balanced-stable (default): Sends each new item to the shortest column and never moves an item once it is placed. This is ideal for feeds where items are appended.
    • balanced: Re-packs all items on every change to achieve the tightest possible layout. Note that this causes items to jump around as they are repositioned.
    • row-first: Forced when virtualization is enabled.
  7. Prevent infinite loops when binding to reactive wrappers

    main

    The MultiSelect component is designed to handle reactive wrappers that clone arrays on assignment (such as Svelte stores, Superforms, or other state management libraries).

    When using bind:selected with a store (e.g., bind:selected={$store}), the component uses internal equality checks (values_equal()) to ensure that updates only trigger when the content actually changes. This prevents the infinite loop cycle that occurs when a wrapper clones an array, triggering a change event, which then triggers another assignment, and so on.

    <script lang="ts">
      import { MultiSelect } from '$lib'
      import { writable } from 'svelte/store'
    
      const options = ['Red', 'Green', 'Blue']
      let list_store = writable([])
    </script>
    
    <!-- Binding directly to a store value via the $ prefix is safe -->
    <MultiSelect {options} bind:selected={$list_store} placeholder="Select colors..." />
  8. Configure ButtonGroup for single-select mode

    main

    By default, ButtonGroup operates in single-select mode (multiple={false}). It behaves as a radiogroup of role="radio" buttons.

    Key behaviors:

    • Keyboard Navigation: One tab stop for the whole group. Arrow keys walk through options (wrapping at ends, skipping disabled ones). Home and End jump to the start/end.
    • Sorting: If sort_order is provided (not null), a sort arrow is rendered. You can style this arrow using sort_button_props (note: host style does not reach this button as it sits outside the radiogroup).
    • Selection: Use bind:selected to manage the single active value.
    <script lang="ts">
      import ButtonGroup, { type ButtonGroupOption } from '$lib/ButtonGroup.svelte'
    
      const options: ButtonGroupOption[] = [
        { value: 'commits', label: 'commits', tooltip: 'Total commits' },
        { value: 'stars', label: 'stars' },
      ]
      let sort_by = $state('commits')
      let sort_order = $state<'asc' | 'desc'>('desc')
    </script>
    
    <ButtonGroup
      {options}
      bind:selected={sort_by}
      bind:sort_order
      label="Sort projects by"
    />
  9. Configure ButtonGroup for multi-select mode

    main

    To enable multi-select, add the multiple prop. This changes the semantics from a radio group to a group of independent toggle buttons with aria-pressed.

    Key behaviors:

    • Selection: bind:selected expects an array of values.
    • Customization: Use the option snippet to replace a button's internal content and the option_suffix snippet to add content alongside the label.
    • Snippets:
      • option({ option, selected }): Controls the button's inner content.
      • option_suffix({ option }): Renders a sibling element wrapped in a .option span. This is useful for non-interactive elements like counts. When used, --btn-group-option-btn-padding-right (default 0.5ex) is applied to the button to make room for the suffix.
    <script lang="ts">
      import ButtonGroup from '$lib/ButtonGroup.svelte'
    
      const tags = { svelte: 'Svelte', kit: 'SvelteKit' }
      let active = $state(['svelte'])
    </script>
    
    <ButtonGroup
      options={tags}
      multiple
      bind:selected={active}
      label="Filter by tag"
    >
      {#snippet option({ option: opt, selected })}
        {opt.label} {selected ? '×' : '+'}
      {/snippet}
      {#snippet option_suffix({ option: opt })}
        <span>Count: 12</span>
      {/snippet}
    </ButtonGroup>
  10. How Toast notifications work

    main

    Toast notifications in this library are managed via a queue rather than a simple list. Only one toast is visible at a time; others wait in a queue ranked by priority. This prevents a burst of notifications from burying important messages or scrolling past before they are read.

    There are two main ways to use them:

    1. Global usage: Use the ready-made toast store to call toast.show('Message') from anywhere in your application.
    2. Scoped usage: Create a ToastStore and pass it to the <Toast /> component to give a specific page or section its own independent queue.

    The underlying logic is a pure reducer (enqueue_toast, dismiss_toast, etc.), and ToastStore is a reactive wrapper that manages the expiry timers.

    import Toast from '$lib/Toast.svelte'
    import { ToastStore } from '$lib/toast-queue.svelte.ts'
    
    const store = new ToastStore()
    // ...
    <Toast {store} />
  11. Event Handling Tips and Behaviors

    main

    Key Behaviors

    • Search Debouncing: The onsearch event is debounced by 150ms to prevent excessive callbacks while the user is typing.
    • Keyboard Navigation: The onactivate event only fires during keyboard navigation (e.g., using arrow keys), not during mouse hover.
    • Custom Options: The oncreate event only triggers when allowUserOptions is enabled and the user types text that does not match any existing options.
    • Duplicate Handling: The onduplicate event only fires when the duplicates prop is set to false (which is the default behavior).
  12. Style the Toc component

    main

    The Toc component provides several prop bags that allow you to pass attributes directly to its internal elements. The component uses :where() for its base styles, meaning your custom classes will outrank the default styles without needing !important.

    <Toc 
      asideProps={{ class: 'my-custom-aside' }}
      navProps={{ class: 'my-custom-nav' }}
      titleProps={{ class: 'my-custom-title' }}
      olProps={{ class: 'my-custom-list' }}
      liProps={{ class: 'my-custom-item' }}
      openButtonProps={{ class: 'my-custom-button' }}
    />