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
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,
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.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/toggle.js";
</script>
<eun-toggle>Bold</eun-toggle>
npm install @eunomia/elements
import "@eunomia/elements/toggle.js";
function BoldToggle() {
return <eun-toggle>Bold</eun-toggle>;
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/toggle.js";
export function BoldToggle() {
return <eun-toggle>Bold</eun-toggle>;
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/toggle.js";
</script>
<template>
<eun-toggle>Bold</eun-toggle>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/toggle.js";
@Component({
selector: "app-bold-toggle",
template: `<eun-toggle>Bold</eun-toggle>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class BoldToggleComponent {}
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
eun-checkboxeun-selectGuidance
- Use a standalone
eun-togglefor one independent on/off choice (a toolbar formatting button, a single filter flag) - Group exactly two related, mutually-exclusive choices in a
eun-toggle-groupfor 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"orappearance="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
variantonly onceappearanceisn'tdefault: pair it with a real semantic meaning (e.g.criticalfor the most urgent choice), not decoration - Always set
aria-labeloneun-toggle-group: see the Accessibility tab
- Using
eun-toggle-groupwith 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-groupto replaceeun-checkboxfor a single binary setting: see Alternatives above, since a loneeun-toggle(oreun-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
variantjust for visual variety, reserve it for choices whose color is actually meaningful, the same restrainteun-button's ownvariantcalls for
Live testing
Properties
Toggle <eun-toggle>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Whether the toggle is currently pressed, or, while swap is set, which of the two values is current |
| swap | boolean | false | Renders 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 |
| active | boolean | false | Indicates whether the component is on disabled state or not. By default, set to false. |
Import the exact TypeScript type behind any property above, see
Slots
| Name | Description |
|---|---|
| (default) | The toggle's content, text, an icon, or both, while swap isn't set |
| unchecked | Content shown while checked is false, while swap is set. A label, an icon, or both |
| checked | Content shown while checked is true, while swap is set. A label, an icon, or both |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired whenever checked changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --toggle-background | Sets the background color while not pressed |
| --toggle-border-color | Sets the border color while not pressed |
| --toggle-border-color-hover | Sets the border color on hover while not pressed, falling back to toggle-border-color when unset, or staying transparent in the flat appearance |
| --toggle-color | Sets the text color while not pressed |
| --toggle-active-background | Sets the background color while pressed. Ignored while swap is set |
| --toggle-active-color | Sets the text color while pressed. Ignored while swap is set |
| --toggle-border-size | Sets the width of the border |
| --toggle-border-radius | Sets the corner radius of the toggle |
| --toggle-padding | Sets the padding of the toggle |
CSS shadow parts
| Name | Description |
|---|---|
| toggle | The 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
| Name | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | The accessible label for the group, required since the group has no visible legend of its own |
| options | Array<EunomiaToggleGroupOption> | [] | The choices to render, in order |
| value | Array<string> | [] | The currently selected choices' values. Always exactly one entry in exclusive mode |
| single | boolean | false | Forces 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 |
| active | boolean | false | Indicates whether the component is on disabled state or not. By default, set to false. |
Import the exact TypeScript type behind any property above, see
Slots
| Name | Description |
|---|---|
| (default) | Toggle item elements, used instead of or alongside options |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired whenever the selection changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --toggle-group-background | Sets the background color of the track. Ignored once appearance isn't default, which has no shared background |
| --toggle-group-border-color | Sets the border color of the track, and the border color of each option at rest in the outline appearance |
| --toggle-group-color | Sets the text color of unselected options |
| --toggle-group-active-background | Sets 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-color | Sets 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-radius | Sets the corner radius of the track and its options |
| --toggle-group-gap | Sets the gap between the options |
| --toggle-group-padding | Sets 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-color | Sets 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-background | Sets the background color of an unselected option on hover. Transparent by default, and applies regardless of appearance |
| --toggle-group-shadow | Sets a box shadow on every option, and the sliding indicator in exclusive mode. None by default |
| --toggle-group-shadow-hover | Sets 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
| Name | Type | Default | Description |
|---|---|---|---|
| value | string | — | The value reported when this option is selected |
| icon | EunomiaIconName | — | An 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 |
| checked | boolean | false | Whether this option is currently selected. Set by the parent toggle group, and shouldn't be set directly |
| active | boolean | false | Indicates whether the component is on disabled state or not. By default, set to false. |
Import the exact TypeScript type behind any property above, see
Slots
| Name | Description |
|---|---|
| (default) | The option's content |
| icon | Replaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty |
CSS shadow parts
| Name | Description |
|---|---|
| item | The item's own surface box, carrying its padding, background, and border. Exposed for effects the item's CSS custom properties don't cover |
Basic
<eun-toggle>Bold</eun-toggle>
document.querySelector("eun-toggle").addEventListener("eunchange", (event) => {
console.log(event.target.checked); // true once pressed
});
Disabled
<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.
<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:
<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:
<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.
<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.
<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.
<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.
<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.
<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:
<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:
<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:
<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.
<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:
<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:
<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:
<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:
<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
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.
<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.
<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>
<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.
<eun-toggle class="custom-toggle" checked>Custom</eun-toggle>
.custom-toggle {
--toggle-active-background: #be185d;
}
<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.
<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 presentaria-pressed="true"|"false": reflectscheckedaria-disabledis not set: a disabled toggle is removed from the tab order entirely instead (see Disabled below), matching a native<button disabled>swapdoesn't change any of the above: it's still a single button witharia-pressedreflectingchecked, just visually presented as a value switch rather than a pressed/not-pressed state. Setaria-labeldirectly 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) orrole="radiogroup"(exclusive mode), labelled by the group's ownaria-label: see Label accessibility below aria-disabledon that same track, mirroring the group'sdisabled
Filter mode entries
role="checkbox"(both the internaloptionsbuttons andeun-toggle-item)aria-checked="true"|"false"per entry, each toggling independently
Exclusive mode entries
role="radio"(both the internaloptionsbuttons andeun-toggle-item) instead ofrole="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 choicessingleallows it to havearia-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 viaTab. Every other entry istabindex="-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