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

error-text replaces help-text in the slot below the group and reddens the track — the same one-slot precedence esa-select and esa-text-field use, so the two messages never stack.

<esa-button-toggle label="Default"></esa-button-toggle>
<esa-button-toggle label="With help" help-text="Pick a layout"></esa-button-toggle>
<esa-button-toggle label="With error" error-text="Choose a layout to continue"></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.

With required, the group reports valueMissing through ElementInternals until something is selected, so the native form refuses to submit and checkValidity() returns false — the asterisk is not the whole of it.

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

API

PropTypeDefaultDescription
label string '' Optional visible label above the group; becomes the radiogroup name.
help-text string '' Helper text below the group. Property: `helpText`.
error-text string '' Validation message below the group. Replaces help-text and reddens the track. Property: `errorText`.
options EsaToggleOption[] [] 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.
size 'xs' | 'sm' | 'md' | 'lg' 'md' Control size (shared form scale).
name string | undefined — Form field name — the key this control submits under.
disabled boolean false Disables every segment.
required boolean false Blocks submission while nothing is selected (valueMissing), and adds an asterisk plus aria-required.

Events

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

Every one of these is a CustomEvent, so the payload is on $event.detail — $event.target.value reads empty, with no error. The names are lowercase and Angular passes them to addEventListener verbatim, so camel-casing one means it never fires.

  • (change)="onChange($event.detail)"

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.
  • 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.