Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Toggle

Toggle is a pressed or not-pressed button, the base building block of the toggle family, usable entirely on its own, such as a "bold" or "italic" toolbar button. Group several together and the selection model changes with nothing else to configure: exactly two choices behave like a single physical switch, with exclusive selection and a sliding indicator, while any other count behaves as an independent multi-select filter, where each choice toggles on its own with no shared indicator, unless single-choice selection is forced regardless of how many choices there are. See the Accessibility tab for the full behavioral and ARIA differences between exclusive and filter mode.

Choosing a different appearance swaps in a different skin entirely, with no shared background and no sliding indicator, each choice its own standalone button instead, optionally colored per choice. See the Button display example.

Dependencies

eun-icon · for a toggle item's own icon, or an options entry's icon
Overview API Examples Accessibility
Bold

When to use

Use a standalone toggle for one independent on/off choice, like a "bold" formatting button. Group toggles when there's a small set of related choices: exactly two for a light/dark-style switch, three or more for a compact filter bar (list, grid, board). Once a filter group grows past five or six choices, select holds up better than a row of toggles.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/toggle.js";
import "@eunomia/elements/toggle-group.js";
<eun-toggle>Bold</eun-toggle>
const group = document.querySelector("eun-toggle-group");
group.options = [
  { label: "Light", value: "light" },
  { label: "Dark", value: "dark" },
];

Importing the files registers <eun-toggle> and <eun-toggle-group> as custom elements, with no further setup needed. It works with any framework, or none, since these are standard web components.

Grouping several toggles under one control? Import @eunomia/elements/toggle-group.js too. Its properties are in the API tab, right below eun-toggle's own, and the Examples tab shows both its two-choice (switch) and three-plus (filter) shapes. eun-toggle-item is documented alongside it: none of the three have a page of their own beyond this one.

Alternatives

You want .. Prefers A single binary setting, not a formatting action eun-checkbox A filter group past five or six choices eun-select

Guidance

  • Use a standalone eun-toggle for one independent on/off choice (a toolbar formatting button, a single filter flag)
  • Group exactly two related, mutually-exclusive choices in a eun-toggle-group for the sliding-switch presentation (light/dark, list/grid, ...)
  • Group three or more independent choices the same way for a compact multi-select filter bar
  • Set appearance="outline" or appearance="flat" when the choices read better as individual buttons than as one shared pill: a priority/urgency picker where each choice carries its own meaning and color (see the Button display example)
  • Give a choice its own variant only once appearance isn't default: pair it with a real semantic meaning (e.g. critical for the most urgent choice), not decoration
  • Always set aria-label on eun-toggle-group: see the Accessibility tab
  • Using eun-toggle-group with exactly two choices when they aren't actually mutually exclusive: a two-choice group can never have both, or neither, selected
  • Reaching for a two-choice eun-toggle-group to replace eun-checkbox for a single binary setting: see Alternatives above, since a lone eun-toggle (or eun-checkbox) is simpler for that
  • Mixing more than a handful of options in one filter group: see Alternatives above
  • Overriding colors with inline styles instead of the --toggle-*/--toggle-group-* CSS variables
  • Giving every choice a different variant just for visual variety, reserve it for choices whose color is actually meaningful, the same restraint eun-button's own variant calls for

Live testing

Properties

Toggle <eun-toggle>

Attributes

NameTypeDefaultDescription
checkedbooleanfalseWhether the toggle is currently pressed, or, while swap is set, which of the two values is current
swapbooleanfalseRenders the toggle as a two value switch, such as light and dark or list and grid, instead of a pressed or not pressed action. The unchecked and checked slots replace the default one, showing whichever content matches the current value instead of the same content with a highlighted background. The pressed background and text color fill is skipped in this mode, since swapping the visible content is meant to be the only sign of the change
appearance'default' | 'flat''default'The toggle's surface style while not pressed, or always while swap is set. Default keeps the toggle's own bordered look. Flat drops the border for the same muted, low emphasis surface used by a flat button, better suited next to borderless buttons
activebooleanfalseIndicates whether the component is on disabled state or not. By default, set to false.

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
(default)The toggle's content, text, an icon, or both, while swap isn't set
uncheckedContent shown while checked is false, while swap is set. A label, an icon, or both
checkedContent shown while checked is true, while swap is set. A label, an icon, or both

Events

NameTypeDescription
eunchangeChangeEventFired whenever checked changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--toggle-backgroundSets the background color while not pressed
--toggle-border-colorSets the border color while not pressed
--toggle-border-color-hoverSets the border color on hover while not pressed, falling back to toggle-border-color when unset, or staying transparent in the flat appearance
--toggle-colorSets the text color while not pressed
--toggle-active-backgroundSets the background color while pressed. Ignored while swap is set
--toggle-active-colorSets the text color while pressed. Ignored while swap is set
--toggle-border-sizeSets the width of the border
--toggle-border-radiusSets the corner radius of the toggle
--toggle-paddingSets the padding of the toggle

CSS shadow parts

NameDescription
toggleThe toggle's own surface box, carrying its padding, background, and border. Exposed for effects the toggle's CSS custom properties don't cover

Into a group

eun-toggle-group combines options and/or slotted eun-toggle-item elements into one control. Its selection model (exclusive switch or multi-select filter) is a function of how many total choices it ends up with (exactly two is always exclusive), or is forced exclusive regardless of count by setting single. appearance is a separate, purely visual axis: default (the default) keeps the shared-pill look above, outline/ flat render each choice as its own standalone button instead, optionally colored per-choice via variant. See the Examples tab for all the shapes and the Accessibility tab for the full behavioral split.

Toggle group <eun-toggle-group>

Attributes

NameTypeDefaultDescription
aria-labelstringThe accessible label for the group, required since the group has no visible legend of its own
optionsArray<EunomiaToggleGroupOption>[]The choices to render, in order
valueArray<string>[]The currently selected choices' values. Always exactly one entry in exclusive mode
singlebooleanfalseForces the exclusive, sliding indicator mode regardless of choice count. Already the case for exactly two choices without setting this
appearance'default' | 'outline' | 'flat''default'The display shape used to render the group's choices
activebooleanfalseIndicates whether the component is on disabled state or not. By default, set to false.

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
(default)Toggle item elements, used instead of or alongside options

Events

NameTypeDescription
eunchangeChangeEventFired whenever the selection changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--toggle-group-backgroundSets the background color of the track. Ignored once appearance isn't default, which has no shared background
--toggle-group-border-colorSets the border color of the track, and the border color of each option at rest in the outline appearance
--toggle-group-colorSets the text color of unselected options
--toggle-group-active-backgroundSets the background color of selected options, and the sliding indicator in exclusive mode, in the default appearance. Also sets the primary variant's selected color in the outline and flat appearances, for an option that sets no variant of its own
--toggle-group-active-colorSets the text color of selected options in the default appearance. Ignored in the outline and flat appearances, where the selected text color always follows the option's own color instead
--toggle-group-border-radiusSets the corner radius of the track and its options
--toggle-group-gapSets the gap between the options
--toggle-group-paddingSets the padding of the track, also used as the indicator's inset so it stays flush with the track's edge. Ignored once appearance isn't default
--toggle-group-hover-colorSets the text color of an unselected option on hover, in the default appearance. Ignored in the outline and flat appearances, where hover and focus text color always follow the option's own color instead, exactly like the selected state
--toggle-group-hover-backgroundSets the background color of an unselected option on hover. Transparent by default, and applies regardless of appearance
--toggle-group-shadowSets a box shadow on every option, and the sliding indicator in exclusive mode. None by default
--toggle-group-shadow-hoverSets the box shadow of an unselected option on hover, falling back to toggle-group-shadow when unset

Into a group, with custom content

eun-toggle-item is the escape hatch for an option whose content doesn't fit options' plain label/icon shape (a custom layout, ...). It must be placed inside a eun-toggle-group; it delegates its own activation to the closest ancestor, which is also the one deciding whether that activation toggles it (filter mode) or exclusively selects it (switch mode).

Toggle item <eun-toggle-item>

Attributes

NameTypeDefaultDescription
valuestringThe value reported when this option is selected
iconEunomiaIconNameAn icon shown before the slotted content. For an icon only item, also set an accessible label yourself, since there's no label property to derive one from
variant'primary' | 'secondary' | 'critical' | 'success' | 'warning'This item's own color, used once the parent group's appearance isn't default. It renders as neutral at rest, taking on this color only once selected, and defaults to primary when unset. Ignored entirely in the default appearance
checkedbooleanfalseWhether this option is currently selected. Set by the parent toggle group, and shouldn't be set directly
activebooleanfalseIndicates whether the component is on disabled state or not. By default, set to false.

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
(default)The option's content
iconReplaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty

CSS shadow parts

NameDescription
itemThe item's own surface box, carrying its padding, background, and border. Exposed for effects the item's CSS custom properties don't cover

Basic

Bold
<eun-toggle>Bold</eun-toggle>
document.querySelector("eun-toggle").addEventListener("eunchange", (event) => {
  console.log(event.target.checked); // true once pressed
});

Disabled

Bold Bold
<eun-toggle disabled>Bold</eun-toggle>
<eun-toggle disabled checked>Bold</eun-toggle>

Independent toggles

Several standalone eun-toggles placed side by side, each one its own independent on/off state, with no eun-toggle-group involved at all. This is the "single element" shape: a text-formatting toolbar, not a set of mutually-related choices.

B I U
<eun-toggle id="bold" aria-label="Bold"><b>B</b></eun-toggle>
<eun-toggle id="italic" aria-label="Italic"><i>I</i></eun-toggle>
<eun-toggle id="underline" aria-label="Underline"><u>U</u></eun-toggle>
document.querySelectorAll("eun-toggle").forEach((toggle) => {
  toggle.addEventListener("eunchange", () => {
    document.execCommand(toggle.id); // or your editor's own command API
  });
});

Value swap

Set swap to repurpose eun-toggle for a two-value switch instead of a pressed/not-pressed action: a light/dark mode control, a list/grid view switch, anything where clicking doesn't "activate" a feature, it just moves between two equally-valid values. The unchecked/checked slots replace the default one, each taking a label, an icon, or both, and whichever matches the current checked value is shown, while the other is removed from layout entirely. Neither value gets the pressed-button fill the default mode applies to checked, since the content change alone signals the switch, exactly like the icon on a real light/dark toggle:

Light Dark
<eun-toggle swap aria-label="Toggle dark mode">
  <span slot="unchecked"
    ><eun-icon name="wb_sunny" size="16"></eun-icon> Light</span
  >
  <span slot="checked"
    ><eun-icon name="bedtime" size="16"></eun-icon> Dark</span
  >
</eun-toggle>
document.querySelector("eun-toggle").addEventListener("eunchange", (event) => {
  document.documentElement.dataset.theme = event.target.checked
    ? "dark"
    : "light";
});

An icon-only version works the same way: set aria-label on the toggle itself for its accessible name, since neither slot has visible text of its own to fall back on:

<eun-toggle swap aria-label="Toggle dark mode">
  <eun-icon slot="unchecked" name="wb_sunny" size="16"></eun-icon>
  <eun-icon slot="checked" name="bedtime" size="16"></eun-icon>
</eun-toggle>

Text-only works too: either slot can hold whatever content fits the switch, independently of the other:

Metric Imperial
<eun-toggle swap aria-label="Switch units">
  <span slot="unchecked">Metric</span>
  <span slot="checked">Imperial</span>
</eun-toggle>

Two choices exclusive

Slot exactly two eun-toggle-item elements (or pass exactly two options, further down) and the group switches its whole selection model: one side is always selected, and a sliding indicator animates behind it.

Light Dark
<eun-toggle-group aria-label="Theme">
  <eun-toggle-item value="light">Light</eun-toggle-item>
  <eun-toggle-item value="dark">Dark</eun-toggle-item>
</eun-toggle-group>

The group defaults to the first entry selected (a switch is never in an undefined position), and clicking the already-active side does nothing: a real switch can't be turned "half off" by pressing its current side again. Read the selection back as value[0], or listen for eunchange:

const theme = document.querySelector("eun-toggle-group");
theme.addEventListener("eunchange", () => console.log(theme.value[0])); // "light" | "dark"

Using options instead

The same two choices, declared with the options property instead of slotted items. Either works, pick whichever is more convenient for how the data arrives. options also reflects as a JSON-serialized HTML attribute, so it can be written directly in markup too, with no JavaScript at all:

<eun-toggle-group
  aria-label="Theme"
  options='[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"}]'
></eun-toggle-group>
// Equivalent, set from JavaScript instead of the HTML attribute above:
document.querySelector("eun-toggle-group").options = [
  { label: "Light", value: "light" },
  { label: "Dark", value: "dark" },
];

Multiple choices exclusive

Set single to get that same exclusive, sliding-indicator presentation for any number of choices, not just two. Exactly one is always selected, the same way: it defaults to the first entry, and clicking (or arrowing onto) the already-active one is a no-op.

Low Medium High
<eun-toggle-group aria-label="Priority" single>
  <eun-toggle-item value="low">Low</eun-toggle-item>
  <eun-toggle-item value="medium">Medium</eun-toggle-item>
  <eun-toggle-item value="high">High</eun-toggle-item>
</eun-toggle-group>

Arrow keys move the roving selection all the way around, wrapping from the last choice back to the first (and vice versa), the same "select on navigate" behavior as a native radio group, just no longer limited to two stops:

const priority = document.querySelector("eun-toggle-group");
priority.addEventListener("eunchange", () => console.log(priority.value[0])); // "low" | "medium" | "high"

Or via options:

<eun-toggle-group
  aria-label="Priority"
  single
  options='[{"label":"Low","value":"low"},{"label":"Medium","value":"medium"},{"label":"High","value":"high"}]'
></eun-toggle-group>

Multi-select filter

Any other total count (one, or three-plus) keeps the group's original behavior: every entry toggles independently, any number (including none, or every one) can be selected at once, exactly like a group of checkboxes. This shape is completely unaffected by the two-choice case above.

List Grid Board
<eun-toggle-group aria-label="View">
  <eun-toggle-item value="list">List</eun-toggle-item>
  <eun-toggle-item value="grid">Grid</eun-toggle-item>
  <eun-toggle-item value="board">Board</eun-toggle-item>
</eun-toggle-group>
const view = document.querySelector("eun-toggle-group");
view.addEventListener("eunchange", () => console.log(view.value)); // e.g. ["list", "board"]

Or the same three choices via the options attribute:

<eun-toggle-group
  aria-label="View"
  options='[{"label":"List","value":"list"},{"label":"Grid","value":"grid"},{"label":"Board","value":"board"}]'
></eun-toggle-group>

Selected by default

Filter mode still allows any number selected, including exactly one: set checked on a single eun-toggle-item (or match one value in value) to start there. Nothing enforces it stays that way: the user can still add a second one, or clear it entirely, exactly like a group of checkboxes where only one happens to be ticked right now. This is different from single above: single makes the selection always exactly one (no adding a second, no clearing it), while this is filter mode's own "any number, including one" behavior, just starting from a state where only one is checked.

List Grid Board
<eun-toggle-group aria-label="View">
  <eun-toggle-item value="list">List</eun-toggle-item>
  <eun-toggle-item value="grid" checked>Grid</eun-toggle-item>
  <eun-toggle-item value="board">Board</eun-toggle-item>
</eun-toggle-group>

Or via value, which pre-checks whichever entries match, however many that turns out to be:

<eun-toggle-group
  aria-label="View"
  value='["grid"]'
  options='[{"label":"List","value":"list"},{"label":"Grid","value":"grid"},{"label":"Board","value":"board"}]'
></eun-toggle-group>

Mixing options and items

Both declaration mechanisms can be combined in the same group: options entries render first, followed by any slotted eun-toggle-item. The total count across both is what decides exclusive vs. multi-select mode. Here that's one option plus two items, three total, so this is filter mode.

Medium High
<eun-toggle-group
  aria-label="Priority"
  options='[{"label":"Low","value":"low"}]'
>
  <eun-toggle-item value="medium">Medium</eun-toggle-item>
  <eun-toggle-item value="high">High</eun-toggle-item>
</eun-toggle-group>

With icons

options supports an icon per entry, something eun-toggle-item has no prop of its own for, since it's meant for freely-composed content instead (put a eun-icon directly in its slot if you need one there). An icon renders icon-only by default, with label supplying its aria-label:

<eun-toggle-group
  aria-label="Alignment"
  options='[
    {"label":"Align left","value":"left","icon":"format_align_left"},
    {"label":"Align center","value":"center","icon":"format_align_center"},
    {"label":"Align right","value":"right","icon":"format_align_right"}
  ]'
></eun-toggle-group>

Set showLabel to render the icon and the label side by side instead:

<eun-toggle-group
  aria-label="Status"
  options='[
    {"label":"Active","value":"active","icon":"check_circle","showLabel":true},
    {"label":"Paused","value":"paused","icon":"pause_circle_filled","showLabel":true}
  ]'
></eun-toggle-group>

Into an item

Set the icon property directly: it renders a leading eun-icon before the slotted content, the same way an options entry's own icon does:

List Grid
<eun-toggle-group aria-label="View">
  <eun-toggle-item value="list" icon="view_list">List</eun-toggle-item>
  <eun-toggle-item value="grid" icon="grid_view">Grid</eun-toggle-item>
</eun-toggle-group>

Icon-only: unlike an options entry, eun-toggle-item has no label of its own to fall back on for an accessible name, so set aria-label directly on the item yourself:

<eun-toggle-group aria-label="Formatting">
  <eun-toggle-item
    value="bold"
    icon="format_bold"
    aria-label="Bold"
  ></eun-toggle-item>
  <eun-toggle-item
    value="italic"
    icon="format_italic"
    aria-label="Italic"
  ></eun-toggle-item>
</eun-toggle-group>

Need something other than a eun-icon, or a different icon library entirely, for that same leading spot? Slot your own content into icon instead. It replaces the prop-driven icon entirely:

List Grid
<eun-toggle-group aria-label="View">
  <eun-toggle-item value="list">
    <svg slot="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line x1="8" y1="6" x2="21" y2="6"></line>
      <line x1="8" y1="12" x2="21" y2="12"></line>
      <line x1="8" y1="18" x2="21" y2="18"></line>
      <line x1="3" y1="6" x2="3.01" y2="6"></line>
      <line x1="3" y1="12" x2="3.01" y2="12"></line>
      <line x1="3" y1="18" x2="3.01" y2="18"></line>
    </svg>
    List
  </eun-toggle-item>
  <eun-toggle-item value="grid">
    <svg slot="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <rect x="3" y="3" width="7" height="7"></rect>
      <rect x="14" y="3" width="7" height="7"></rect>
      <rect x="14" y="14" width="7" height="7"></rect>
      <rect x="3" y="14" width="7" height="7"></rect>
    </svg>
    Grid
  </eun-toggle-item>
</eun-toggle-group>

For anything beyond a single leading icon (one placed after the text, several icons, or any other custom layout), slot your own eun-icon directly instead of using the icon property:

Ascending Descending
<eun-toggle-group aria-label="Sort">
  <eun-toggle-item value="asc">
    Ascending
    <eun-icon name="arrow_upward" size="16"></eun-icon>
  </eun-toggle-item>
  <eun-toggle-item value="desc">
    Descending
    <eun-icon name="arrow_downward" size="16"></eun-icon>
  </eun-toggle-item>
</eun-toggle-group>

Button display

Set appearance to swap the shared-pill presentation for standalone buttons, useful once each choice deserves its own visual weight (a priority/urgency picker, a set of clearly distinct actions) rather than reading as one segmented control. This is purely a skin: the exclusive/filter selection rule, every ARIA role and keyboard interaction, stays exactly the same regardless of appearance. See the Accessibility tab.

  • default (the default): every choice shares one bordered background, with a sliding highlight behind the active one in exclusive mode.
  • outline: no shared background, no indicator, and each choice is its own outlined button, neutral at rest, taking on its color once selected.
  • flat: the same standalone buttons, borderless at rest instead.
List Grid Board
<eun-toggle-group aria-label="View" appearance="outline">
  <eun-toggle-item value="list">List</eun-toggle-item>
  <eun-toggle-item value="grid">Grid</eun-toggle-item>
  <eun-toggle-item value="board">Board</eun-toggle-item>
</eun-toggle-group>

flat drops the border entirely, at rest and once selected alike; only the tinted background and text color signal the choice:

List Grid Board
<eun-toggle-group aria-label="View" appearance="flat">
  <eun-toggle-item value="list">List</eun-toggle-item>
  <eun-toggle-item value="grid">Grid</eun-toggle-item>
  <eun-toggle-item value="board">Board</eun-toggle-item>
</eun-toggle-group>

appearance composes with every selection model above, unchanged: two choices (or single) still behave exclusively, any other count still filters; only the skin changes. Here, the same exclusive two-choice switch from earlier, as standalone buttons instead of a sliding pill:

Light Dark
<eun-toggle-group aria-label="Theme" appearance="outline">
  <eun-toggle-item value="light">Light</eun-toggle-item>
  <eun-toggle-item value="dark">Dark</eun-toggle-item>
</eun-toggle-group>

Separate colors

Once appearance isn't default, set variant (the same five semantic colors eun-button itself offers) on an individual options entry or eun-toggle-item, independently of its neighbors. A choice with no variant of its own still works, it just defaults to primary; nothing requires every choice to opt in. This is what an urgency/priority picker looks like, exactly the shape eun-scheduler-event-editor uses for its own priority field:

Low Medium High Critical
<eun-toggle-group aria-label="Priority" appearance="outline" single>
  <eun-toggle-item value="low" variant="success">Low</eun-toggle-item>
  <eun-toggle-item value="medium">Medium</eun-toggle-item>
  <eun-toggle-item value="high" variant="warning">High</eun-toggle-item>
  <eun-toggle-item value="critical" variant="critical"
    >Critical</eun-toggle-item
  >
</eun-toggle-group>

Or the same choices via options, each entry carrying its own variant :

<eun-toggle-group
  aria-label="Priority"
  appearance="outline"
  single
  options='[
    {"label":"Low","value":"low"},
    {"label":"Medium","value":"medium","variant":"secondary"},
    {"label":"High","value":"high","variant":"warning"},
    {"label":"Critical","value":"critical","variant":"critical"}
  ]'
></eun-toggle-group>

A filter (multi-select) group works the same way: variant is entirely independent of exclusive vs. filter mode:

Active Paused Failed
<eun-toggle-group aria-label="Status" appearance="flat">
  <eun-toggle-item value="active" variant="success">Active</eun-toggle-item>
  <eun-toggle-item value="paused">Paused</eun-toggle-item>
  <eun-toggle-item value="failed" variant="critical">Failed</eun-toggle-item>
</eun-toggle-group>

variant is ignored in default mode: with a shared pill and a single --toggle-group-active-background, there's no per-choice surface for an individual color to apply to.

Part of the shared vocabulary covered in Variants, alongside every other component that reuses it.

Programmatic selection

Setting .value from script drives the group exactly like a click would. In exclusive mode it takes the array's first matching entry, while in multi-select mode, every value present gets checked and every other one gets unchecked.

List Grid Board Select Grid + Board
<eun-toggle-group aria-label="View" id="view">
  <eun-toggle-item value="list">List</eun-toggle-item>
  <eun-toggle-item value="grid">Grid</eun-toggle-item>
  <eun-toggle-item value="board">Board</eun-toggle-item>
</eun-toggle-group>
<eun-button id="select-grid-board">Select Grid + Board</eun-button>
document.querySelector("#select-grid-board").addEventListener("click", () => {
  document.querySelector("#view").value = ["grid", "board"];
});

Note that assigning .value this way (like a native <input>.value = ...) doesn't dispatch eunchange on its own. That event is reserved for user-driven activation (a click, a keypress). Call any dependent update logic directly after the assignment instead of relying on the event.

Disabled

Setting disabled on the group forces every slotted item disabled too. An individually disabled item, on the other hand, stays disabled even if the group itself isn't.

List Grid
<eun-toggle-group aria-label="View" disabled>
  <eun-toggle-item value="list">List</eun-toggle-item>
  <eun-toggle-item value="grid">Grid</eun-toggle-item>
</eun-toggle-group>
Low Medium High
<eun-toggle-group aria-label="Priority">
  <eun-toggle-item value="low">Low</eun-toggle-item>
  <eun-toggle-item value="medium" disabled>Medium</eun-toggle-item>
  <eun-toggle-item value="high">High</eun-toggle-item>
</eun-toggle-group>

Custom colors

Override the --toggle-*/--toggle-group-* CSS variables: full list in the API tab.

Custom
<eun-toggle class="custom-toggle" checked>Custom</eun-toggle>
.custom-toggle {
  --toggle-active-background: #be185d;
}
Light Dark
<eun-toggle-group aria-label="Theme" class="custom-group">
  <eun-toggle-item value="light">Light</eun-toggle-item>
  <eun-toggle-item value="dark">Dark</eun-toggle-item>
</eun-toggle-group>
.custom-group {
  --toggle-group-active-background: #be185d;
  --toggle-group-active-color: #fff;
}

Hover color

--toggle-hover-color/--toggle-hover-background (standalone) and --toggle-group-hover-color/--toggle-group-hover-background (group options and eun-toggle-item, sharing the same variables) only apply while unpressed/unselected and enabled. Hover the elements below:

--toggle-group-hover-color only has an effect in default mode, though. Once appearance is outline or flat, a choice's hover text color (and its :focus-visible outline/text color) always follow its own variant color instead (see the Button display example), the same color it takes on once selected: hovering or tabbing to "Critical" previews critical's red, not a generic accent, whether or not it ends up chosen. --toggle-group-hover-background still applies in every appearance.

Hover me List Grid Board
<eun-toggle class="custom-hover">Hover me</eun-toggle>
<eun-toggle-group aria-label="View" class="custom-group-hover">
  <eun-toggle-item value="list">List</eun-toggle-item>
  <eun-toggle-item value="grid">Grid</eun-toggle-item>
  <eun-toggle-item value="board">Board</eun-toggle-item>
</eun-toggle-group>
.custom-hover {
  --toggle-hover-color: #be185d;
  --toggle-hover-background: #fdf2f8;
}
.custom-group-hover {
  --toggle-group-hover-color: #be185d;
  --toggle-group-hover-background: #fdf2f8;
}

This audit covers all three shapes of the toggle family: a standalone eun-toggle, a eun-toggle-group in exclusive/switch mode (exactly two total choices, or any count with single set), and one in multi-select/ filter mode (any other count, with single unset), since grouping changes both the interaction model and the accessibility tree built around it.

Keyboard interactions

Context Key Action
Standalone eun-toggle Tab / Shift+Tab Moves focus in/out (single tab stop)
Standalone eun-toggle Enter / Space Toggles checked
Filter mode Tab / Shift+Tab Moves between choices: each is its own tab stop, same as a group of checkboxes
Filter mode Enter / Space Toggles the focused choice on/off
Exclusive mode Tab / Shift+Tab Moves focus in/out of the group: a single tab stop for the whole group, regardless of choice count
Exclusive mode ArrowLeft / ArrowUp Moves the selection (and focus) to the previous choice, wrapping from the first to the last, select-on-navigate, like a native radio group
Exclusive mode ArrowRight / ArrowDown Moves the selection (and focus) to the next choice, wrapping from the last to the first
Exclusive mode Home / End Jumps to the first / last choice

With exactly two choices, "previous" and "next" both reduce to the same flip between the two: there's nowhere else to go. A disabled choice is skipped over automatically when arrowing (or Home/End-ing) past it, matching a native radiogroup, though this only becomes observable once single allows three or more choices. With exactly two, the other one is either enabled (nothing to skip) or it isn't (nothing to move to either way).

Filter mode's internal options buttons are real <button> elements, so Enter/Space already work natively. Only slotted eun-toggle-item elements (not native buttons) need eun-toggle-group's own explicit handling for that.

ARIA attributes

None of the three wrap a native <input> or <button>: the host element itself is the control in every case, so every ARIA state lives directly on it:

Standalone eun-toggle

  • role="button", set on connect if not already present
  • aria-pressed="true"|"false": reflects checked
  • aria-disabled is not set: a disabled toggle is removed from the tab order entirely instead (see Disabled below), matching a native <button disabled>
  • swap doesn't change any of the above: it's still a single button with aria-pressed reflecting checked, just visually presented as a value switch rather than a pressed/not-pressed state. Set aria-label directly on the toggle for an icon-only pair (see the Value swap example): neither slot has visible text of its own for the accessible name to fall back on.

eun-toggle-group, both modes

  • The internal track renders role="group" (filter mode) or role="radiogroup" (exclusive mode), labelled by the group's own aria-label: see Label accessibility below
  • aria-disabled on that same track, mirroring the group's disabled

Filter mode entries

  • role="checkbox" (both the internal options buttons and eun-toggle-item)
  • aria-checked="true"|"false" per entry, each toggling independently

Exclusive mode entries

  • role="radio" (both the internal options buttons and eun-toggle-item) instead of role="checkbox": the whole point of this mode is that it behaves like a radio group, not a set of independent checkboxes, regardless of how many choices single allows it to have
  • aria-checked="true"|"false" per entry, always exactly one "true"
  • A roving tabindex: only the selected entry (or the first, before one is ever selected) is reachable via Tab. Every other entry is tabindex="-1", reachable only by arrowing to it, which is what makes the whole group a single tab stop no matter how many choices it has

Label accessibility

eun-toggle-group has no visible legend of its own (unlike eun-radio-list/eun-checkbox-list, which render one from their own label property), so always set aria-label directly on eun-toggle-group itself. Without it, a screen reader announces "group" or "radio group" with no further context, leaving the user unable to tell what the choice is even about.

For an icon-only options entry (icon set, showLabel unset), label still supplies the button's aria-label and title even though it isn't rendered visibly. It stays the accessible name and hover tooltip either way. eun-toggle-item has an icon property too, but no equivalent label to derive a name from when used icon-only (with an empty default slot): set aria-label on the item itself in that case.

Disabled

disabled removes the control from the tab order entirely (standalone eun-toggle, or an individual filter-mode entry) rather than exposing aria-disabled on a still-focusable element, matching a native <button disabled>, which behaves the same way. Setting disabled on eun-toggle-group itself forces every slotted eun-toggle-item disabled too, so a consumer never has to repeat it on each one. An item individually marked disabled, on the other hand, stays that way even once the group itself isn't.

There is no readonly concept anywhere in the toggle family: unlike the form fields (eun-checkbox, eun-radio, ...), these are action controls with no notion of "visible but not currently editable."

Exclusive mode is never in an undefined state

A eun-toggle-group in exclusive mode (two total choices, or any count with single set) always has exactly one of them selected, starting from first paint (defaulting to the first one, or to whichever entry was authored checked directly), never more than one, never none. This mirrors a real physical switch, which is always physically in one position or another, whether it has two positions or several. Clicking the currently active choice, or arrowing onto it, is a deliberate no-op (and doesn't dispatch eunchange) for the same reason a native radio can't be "unchecked" by clicking it again.

Button display (appearance) changes nothing above

appearance="outline"/appearance="flat" (see the Button display example) is a skin swap only: every role, every aria-checked/aria-disabled state, the roving tabindex in exclusive mode, and every keyboard interaction listed above stays byte-for-byte identical to default mode. The only difference is which CSS rules paint the selected entry, since there's no alternate ARIA pattern to learn for it.

A selected entry is never signaled by color (hue) alone, in any appearance. It also gains a heavier font-weight, a background fill, and, in outline mode, a border that a neutral, unselected entry doesn't have, on top of the underlying aria-checked/role state a screen reader announces regardless of any visual styling. variant's five colors (primary, secondary, critical, success, warning) are not chosen to be distinguishable from each other for color-blind users on hue alone either, so don't rely on variant as the only way a choice's meaning is communicated. Pair it with a distinguishing label (as in the Priority example), not color alone.

In outline/flat mode, hover and :focus-visible also pick up a choice's own variant color (see Hover color in the Examples tab), so a choice previews its selected color before it's actually chosen. This never removes the focus ring itself, only recolors it. :focus-visible still always renders a visible outline, so keyboard focus stays as visible as in default mode (WCAG 2.4.7) regardless of which variant a given choice has.

Reduced motion

The sliding #indicator (exclusive mode only) transitions its position and size over ~250ms by default. Under prefers-reduced-motion: reduce, that transition is dropped. Only its opacity fade (appearing/disappearing) still animates, briefly, so the indicator doesn't pop in and out instantly while everything else on the page still respects the same setting.

Reference links

WAI-ARIA Authoring Practices: Button Pattern (incl. Toggle Button)
WAI-ARIA Authoring Practices: Radio Group Pattern
WAI-ARIA Authoring Practices: Checkbox Pattern
WAI Web Accessibility Tutorials: Labeling Controls