Signal FormsStandalone

Segmented

Single-choice picker rendered as a segmented control. A signal-forms native control — it implements FormValueControl, so [formField] binds straight to its value model. [(value)] works standalone, and [(ngModel)] / reactive forms keep working through Angular's bridge.

Installation

import { WrSegmented } from 'ngwr/segmented';

@Component({ imports: [WrSegmented] })
export class MyComponent {}

Basic usage

Value: week
<wr-segmented [options]="options" [(value)]="range" />

With icons

Value: light
<wr-segmented
  [options]="[
    { value: 'light', label: 'Light', icon: 'sun' },
    { value: 'dark', label: 'Dark', icon: 'moon' },
  ]"
  [(value)]="mode"
/>

Signal Forms

[formField] writes the component's own value model — there is no ControlValueAccessor in between. Inside a <wr-form-field> the label points at the first segment (a <label for> may only name a labelable element, and <wr-segmented> is not one), while aria-describedby and aria-invalid land on the group, because the strip is one field rather than a row of them. touch fires when focus leaves the strip, which is what lets the field show its copy.

import { Component, signal } from '@angular/core';
import { FormField, form, required } from '@angular/forms/signals';
import { WrFormField } from 'ngwr/form';
import { WrSegmented, type WrSegmentedOption } from 'ngwr/segmented';

@Component({
  selector: 'app-schedule',
  imports: [FormField, WrFormField, WrSegmented],
  template: `
    <wr-form-field label="Range">
      <wr-segmented [options]="ranges" [formField]="schedule.range" />
    </wr-form-field>
  `,
})
export class ScheduleComponent {
  protected readonly ranges: readonly WrSegmentedOption<string>[] = [
    { value: 'day', label: 'Day' },
    { value: 'week', label: 'Week' },
    { value: 'month', label: 'Month' },
  ];

  private readonly model = signal({ range: '' });
  protected readonly schedule = form(this.model, path => {
    required(path.range);
  });
}

Accessibility

The host stays a role="group" of aria-pressed toggle buttons. Being a form control changes what the value binds to, not what the widget is — a radiogroup would owe a single roving tab stop and arrow-key selection, a different keyboard contract. Each enabled segment is its own tab stop, and Enter / Space activate it as the browser's own default action on a button.

Types

Data shapes used by the inputs and outputs above.

interface WrSegmentedOption<T = unknown> {
  value: T;
  label?: string;
  icon?: WrIconName;
  disabled?: boolean;
}
NameDescriptionTypeDefault
WrSegmentedOptionOne entry in the track.interface
valuerequiredModel value when this segment is picked.T
labelVisible text; omit for icon-only segments.string
iconLeading icon.WrIconName
disabledDisable this segment.booleanfalse

API

NameDescriptionTypeDefault
optionsrequiredThe segments to render.readonly WrSegmentedOption<T>[]
valueThe picked segment's value, null when nothing is selected. Bound by [formField], or two-way via [(value)].T | nullnull
(touch)Emitted when focus leaves the strip, so a bound field can mark itself touched.void
disabledDisable the whole control. Bound automatically from the field's disabled state when used with [formField].booleanfalse
readonlyRefuse changes while every segment stays focusable and the value still submits. Bound automatically from the field's readonly state when used with [formField]. NOTHING is mirrored into ARIA here, deliberately: the strip is a role="group" of aria-pressed buttons, and ARIA defines aria-readonly for neither role — it is a state of checkbox / radiogroup / textbox and their kin, and it is not global. aria-disabled is the only attribute that would apply to a button, and it says the wrong thing: these segments are still focusable and the value still submits. So the state is honest in the DOM (wr-segmented--readonly) and silent in the accessibility tree rather than announced with a word that means something else.booleanfalse
sizeControl size — shares the --wr-control-* contract.WrSegmentedSize'md'

CSS variables

Custom properties ngwr/segmented publishes. Each default below is declared on the component's own selector, so a :root override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.

VariableDefaultDeclared on
--wr-segmented-bgvar(--wr-color-fill).wr-segmented
--wr-segmented-colorvar(--wr-color-on-surface).wr-segmented
--wr-segmented-font-sizevar(--wr-text-sm).wr-segmented +2 variant overrides
--wr-segmented-line-heightvar(--wr-control-line-height-md).wr-segmented +2 variant overrides
--wr-segmented-option-py0.125rem.wr-segmented +2 variant overrides
--wr-segmented-padding0.1875rem.wr-segmented +2 variant overrides
--wr-segmented-radius0.5rem.wr-segmented +2 variant overrides
--wr-segmented-selected-bgvar(--wr-color-surface).wr-segmented
--wr-segmented-selected-colorvar(--wr-color-on-surface).wr-segmented
--wr-segmented-thumb-count1.wr-segmented
--wr-segmented-thumb-index0.wr-segmented
--wr-segmented-thumb-radiuscalc(var(--wr-segmented-radius) - var(--wr-segmented-padding)).wr-segmented