Toggle — <mv-toggle-group>

Toggle button .mv-toggle (aria-pressed, variants, icon that swaps with state) and <mv-toggle-group>, single or multiple: roving tabindex, value, attached group, and a segmented control whose indicator slides between options.

CategoryPrimitives
TypeWeb Component (<mv-toggle-group>)
Statusstable
Keywordstoggle, toggle-group, segmented-control, toolbar, aria-pressed, roving-tabindex

When to use

Avoid when

Install

node scripts/add.mjs toggle --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["toggle"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/element.js, core/focus.js, core/observe.js, components/toggle/toggle.js, components/toggle/toggle.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="tg-demo" style="display:grid;gap:1.75rem;justify-items:center">
  <div style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center;justify-content:center">
    <mv-toggle-group type="multiple" aria-label="Text formatting">
      <button class="mv-toggle" value="bold" aria-label="Bold" aria-pressed="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/></svg></button>
      <button class="mv-toggle" value="italic" aria-label="Italic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 4h-9M14 20H5M15 4 9 20"/></svg></button>
      <button class="mv-toggle" value="underline" aria-label="Underline"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 4v6a6 6 0 0 0 12 0V4M4 20h16"/></svg></button>
      <button class="mv-toggle" value="strikethrough" aria-label="Strikethrough"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M16 5.5A4.5 3.5 0 0 0 12 4c-2.8 0-4.5 1.5-4.5 3.5 0 1.3.8 2.3 2 3M4 12h16M8 18.5A4.5 3.5 0 0 0 12 20c2.8 0 4.5-1.5 4.5-3.5 0-.6-.1-1.1-.4-1.5"/></svg></button>
    </mv-toggle-group>

    <mv-toggle-group data-variant="outline" value="left" required aria-label="Alignment">
      <button class="mv-toggle" value="left" aria-label="Align left"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M4 12h10M4 18h13"/></svg></button>
      <button class="mv-toggle" value="center" aria-label="Align center"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M7 12h10M5.5 18h13"/></svg></button>
      <button class="mv-toggle" value="right" aria-label="Align right"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M10 12h10M7 18h13"/></svg></button>
      <button class="mv-toggle" value="justify" aria-label="Justify" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
    </mv-toggle-group>
  </div>

  <mv-toggle-group data-variant="segmented" value="week" aria-label="Period">
    <button class="mv-toggle" value="day">Day</button>
    <button class="mv-toggle" value="week">Week</button>
    <button class="mv-toggle" value="month">Month</button>
    <button class="mv-toggle" value="year">Year</button>
  </mv-toggle-group>

  <mv-toggle-group data-variant="segmented" data-size="sm" value="system" aria-label="Theme">
    <button class="mv-toggle" value="light"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>Light</button>
    <button class="mv-toggle" value="dark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>Dark</button>
    <button class="mv-toggle" value="system"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>System</button>
  </mv-toggle-group>

  <div style="display:flex;gap:.5rem;align-items:center">
    <button class="mv-toggle" data-variant="accent" aria-pressed="true" data-mv-toggle>
      <svg data-off viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1 6.2L12 17.3 6.5 20.2l1-6.2L3 9.6l6.2-.9z"/></svg>
      <svg data-on viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1 6.2L12 17.3 6.5 20.2l1-6.2L3 9.6l6.2-.9z"/></svg>
      Favorite
    </button>
    <button class="mv-toggle" data-variant="outline" aria-pressed="false" data-mv-toggle>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 17v5M9 10.8V4h6v6.8l3 3.2v2H6v-2z"/></svg>
      Pin
    </button>
    <button class="mv-toggle" data-size="sm" aria-pressed="false" data-mv-toggle>Captions</button>
  </div>
</div>

API

Attributes

NameTypeDefaultDescription
typesingle | multiplesingleOn the group: one option or several.
valuestringInitial value (comma-separated list in multiple mode). Otherwise read from the children’s aria-pressed="true".
requiredbooleanSingle mode: can’t deselect everything (implied with segmented).
orientationhorizontal | verticalhorizontalArrow-key axis and layout.
disabledbooleanDisables the whole group (then restores individually disabled items).
data-variantoutline | segmentedGroup: attached with a border, or a segmented control with an animated indicator.
data-sizesm | lgOn the group or on a .mv-toggle.
data-variant (on .mv-toggle)outline | accentStyle of a standalone button; accent = brand tint once pressed.
data-shapesquareForces a square button (automatic for a button with an aria-label and a single icon).
data-mv-togglebooleanOn a standalone .mv-toggle: toggles aria-pressed on click automatically and emits mv-change { pressed }.
value (on buttons)stringOption identifier (otherwise data-value, otherwise the text).

Properties

NameTypeDescription
valuestring | string[]Single: string ("" if none); multiple: array.
itemsHTMLElement[]Direct child buttons.

Events

NameDescription
mv-changeGroup: detail { value, item } after a user action. Standalone button (data-mv-toggle): detail { pressed }.

Content structure

NameDescription
<button class="mv-toggle" value="…">Direct children of the group. [data-on] / [data-off] children shown according to state.

CSS classes

NameDescription
mv-toggleToggle button styled from aria-pressed.
mv-toggle-group-indicatorSliding pill generated in the segmented variant.

CSS variables

NameDefaultDescription
--mv-toggle-radiusvar(--mv-radius-md)Button radius.
--mv-toggle-group-radiusvar(--mv-radius-lg)Segmented control radius.

Accessibility

Real <button>s with aria-pressed (read as “toggle button, pressed”). The group has role="group" (add an aria-label) and a single tab stop: arrow keys and Home/End to navigate, Space/Enter to toggle; the tab stop lands on the pressed option. Icon buttons: aria-label required. The indicator is decorative (aria-hidden) and has no transition with reduced motion.