Toggle button - headless

Styled SvToggleButton plus a custom on/off pill, one bound pressed value with a readout.

A live, editable Svelte 5 component example from the SvGrid gallery (Headless Editors). Read the SvGrid UI component docs for the full API.

About this example

The headless toggle button core of the SvGrid UI kit in Svelte 5. createToggle is the state machine behind SvToggleButton, pressed on and off with aria-pressed, exposed as buttonProps to spread onto any element. The same core drives the styled component and a custom on and off pill, both bound to one pressed value with a readout.

Headless toggle button - the SvGrid UI kit is headless-first + render-ready, just like the grid. createToggle is the state machine behind <SvToggleButton>: pressed on/off + ARIA, exposed as **prop-getters** you spread onto YOUR OWN markup. Below, the same core drives the styled component AND a totally custom render, both bound to one value.

Imports, features and API used

Imports: @svgrid/grid

Frequently asked questions

What does buttonProps include?

type button, aria-pressed reflecting the state, a click handler that flips it and keyboard handling for Space and Enter, so any element becomes an accessible toggle.

Can the change be vetoed?

Yes. The core calls onChange with the requested state and the bound value is yours to update or not, which is how a controlled toggle stays controlled.

Why a core for something this small?

So the toggle in a grid cell, a toolbar and a custom pill share one ARIA and keyboard implementation rather than three.

Related documentation

Related articles

  • SvGrid vs TanStack Table - A Deep Dive - A concrete architectural comparison of SvGrid and TanStack Table's Svelte adapter - how each handles reactivity, rendering, and feature composition, with code that shows exactly where they diverge.
  • SvGrid vs svelte-headless-table - A practical comparison of SvGrid and svelte-headless-table covering reactivity model, rendering approach, feature scope, and when each is the right choice for a Svelte 5 project.
  • SvGrid UI Components Tips and Tricks: Svelte 5 Buttons, Inputs, Tree, Forms - Practical tips for the SvGrid UI component kit - SvButton states, a Cmd+K command palette, a virtualized searchable tree, schema-driven forms, a parsing date/time field, and the shared field contract - each with a code snippet.

Source code (271-headless-togglebutton.svelte)

<script lang="ts">
  /**
   * Headless toggle button - the SvGrid UI kit is headless-first + render-ready,
   * just like the grid. `createToggle` is the state machine behind
   * <SvToggleButton>: pressed on/off + ARIA, exposed as **prop-getters** you
   * spread onto YOUR OWN markup. Below, the same core drives the styled
   * component AND a totally custom render, both bound to one value.
   */
  import { SvToggleButton, createToggle } from '@svgrid/grid'

  let value = $state(false)

  // The headless core - identical behavior, our own DOM.
  const t = createToggle({
    pressed: () => value,
    onChange: (v) => (value = v),
  })
</script>

<div class="wrap">
  <header>
    <h2>Headless toggle button</h2>
    <p>
      Every editor ships a headless core + a styled component - the same split as the grid
      (<code>createSvGrid</code> / <code>&lt;SvGrid&gt;</code>). Here <code>createToggle</code>
      drives both renders below; the pressed state and ARIA come from the core, the markup is yours.
    </p>
  </header>

  <div class="cols">
    <section>
      <h3>Styled <code>&lt;SvToggleButton&gt;</code></h3>
      <SvToggleButton pressed={value} ariaLabel="Bold" onChange={(v) => (value = v)}>Bold</SvToggleButton>
    </section>

    <section>
      <h3>Your markup, same core</h3>
      <button class="pill" class:on={t.pressed} {...t.buttonProps()}>
        <span class="dot"></span>{t.pressed ? 'On' : 'Off'}
      </button>
      <p class="hint">Tab in, then Space/Enter flips the state - all from <code>createToggle</code>.</p>
    </section>
  </div>

  <aside class="readout">
    <h3>Pressed</h3>
    <div class="tags"><span class="tag">{value}</span></div>
  </aside>
</div>

<style>
  .wrap { padding: 20px; max-width: 900px; display: flex; flex-direction: column; gap: 18px; }
  header h2 { margin: 0 0 4px; font-size: 20px; font-weight: 700; }
  header p { margin: 0; color: var(--sg-muted, #64748b); font-size: 13.5px; line-height: 1.55; max-width: 680px; }
  code { background: var(--sg-row-hover-bg, #eef2ff); padding: 1px 5px; border-radius: 5px; font-size: 12px; }
  .cols { display: flex; gap: 32px; flex-wrap: wrap; align-items: flex-start; }
  section h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--sg-muted, #64748b); }
  /* Fully custom render - nothing shared with SvToggleButton but the core. */
  .pill {
    display: inline-flex; align-items: center; gap: 8px; padding: 9px 18px; border-radius: 999px;
    font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
    border: 1px solid var(--sg-border, #e2e8f0); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
    transition: background 0.12s, border-color 0.12s, color 0.12s;
  }
  .pill .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--sg-border, #cbd5e1); transition: background 0.12s; }
  .pill.on { background: var(--sg-accent, #4f46e5); color: var(--sg-on-accent, #fff); border-color: transparent; }
  .pill.on .dot { background: var(--sg-on-accent, #fff); }
  .pill:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #4f46e5)); outline-offset: 2px; }
  .hint { margin: 10px 0 0; font-size: 12px; color: var(--sg-muted, #94a3b8); line-height: 1.5; }
  .readout h3 { margin: 0 0 8px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--sg-muted, #64748b); }
  .tags { display: flex; gap: 6px; flex-wrap: wrap; }
  .tag { padding: 3px 10px; border-radius: 6px; font-size: 13px; background: var(--sg-row-hover-bg, #f1f5f9); }
</style>

View this example on GitHub

More Headless Editors examples

  • Headless editors - Headless-first, like the grid: createListbox is the state machine behind SvListBox (roving focus, single/multi selection, keyboard, ARIA) exposed as prop-getters you spread onto YOUR own markup. One core drives both the styled SvListBox and a custom chip-cloud render, bound to one value.
  • Combobox - headless - createCombobox drives the styled SvComboBox and a custom filter-input render, both bound to one value with a readout.
  • Dropdown list - headless - createDropdownList drives SvDropDownList and a custom trigger+menu render, sharing one value.
  • Autocomplete - headless - createAutocomplete drives SvAutoComplete and a custom suggestion-list render over one text value.
  • Tags input - headless - createTagsInput drives SvTagsInput and a custom chip-cloud render, both editing one string array.