Prototype Library

Button Toggle

A segmented single-select — one-of-N choices rendered as a row of connected buttons. Form-associated, holds the selected value, and follows the WAI-ARIA radiogroup pattern with arrow-key navigation.

Inherited wc import '@esa/ecology/esa-button-toggle';

Preview

<esa-button-toggle label="View" id="bt-preview"></esa-button-toggle>

<script>
  el.options = [
    { value: 'list', label: 'List' },
    { value: 'grid', label: 'Grid' },
    { value: 'map', label: 'Map' },
  ];
  el.value = 'grid';
</script>

Sizes

Four sizes share the form scale. Default is md.

<esa-button-toggle size="xs"></esa-button-toggle>
<esa-button-toggle size="sm"></esa-button-toggle>
<esa-button-toggle size="md"></esa-button-toggle>
<esa-button-toggle size="lg"></esa-button-toggle>

Icons

Each option may carry an icon — inner Lucide SVG markup (the same paths convention as esa-icon). It renders before the label and inherits currentColor, so it flips to the inverse color on the selected segment.

el.options = [
  { value: 'grid', label: 'Grid', icon: "<rect width='18' height='18' x='3' y='3' rx='2'/>…" },
  { value: 'kanban', label: 'Kanban', icon: "<path d='M3 3h18v18H3z'/>…" },
  { value: 'timeline', label: 'Timeline', icon: "<path d='M8 2v4'/>…" },
];

Omit label for an icon-only segment — set ariaLabel so it still has an accessible name.

el.options = [
  { value: 'grid', icon: "<rect …/>", ariaLabel: 'Grid' },
  { value: 'kanban', icon: "<path …/>", ariaLabel: 'Kanban' },
  { value: 'timeline', icon: "<path …/>", ariaLabel: 'Timeline' },
];

States

<esa-button-toggle label="Default"></esa-button-toggle>
<esa-button-toggle label="With hint" hint="Pick a layout"></esa-button-toggle>
<esa-button-toggle label="Disabled" disabled></esa-button-toggle>

Form participation

The control submits its selected value under its name. Submitting logs the view entry.

<form>
  <esa-button-toggle name="view" label="View"></esa-button-toggle>
  <button type="submit">Submit</button>
</form>

API

PropTypeDefaultDescription
options { value, label, icon?, ariaLabel? }[] [] The segments. Set as a property (an array, not an attribute). Optional icon is inner Lucide SVG markup (the same paths convention as esa-icon), rendered before the label and inheriting currentColor. Omit label for an icon-only segment, but then set ariaLabel for its accessible name.
value string '' Selected option's value. Single-select; bound to the form value.
label string '' Optional visible label above the group; becomes the radiogroup name.
hint string '' Optional helper text below the group.
size 'xs' | 'sm' | 'md' | 'lg' 'md' Control size (shared form scale).
required boolean false Marks the field required (adds an asterisk and aria-required).
disabled boolean false Disables every segment.

Events

EventTypeDefaultDescription
change CustomEvent<{ value: string }> Fired when the selection changes. Composed and bubbling; detail.value is the newly selected value.

Accessibility

  • Renders role="radiogroup" wrapping role="radio" buttons with aria-checked reflecting selection.
  • Roving tabindex: only the selected (or first) segment is tab-reachable; Arrow keys move selection through the group, wrapping at the ends.
  • Home and End jump to the first and last segments; Enter and Space select the focused segment.
  • The label becomes the group's accessible name via aria-labelledby; hint is wired with aria-describedby.
  • Visible focus ring on the active segment via --focus-ring-* tokens; the focused segment is raised so its ring sits above neighbors.
  • Best for small fixed option sets — past about six options, prefer a select.