Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Segment Group

One choice out of a few, drawn as a bar of segments with a pill under the chosen one.

Rendered by React 19.3.1

Loading React…
import { SegmentGroup } from '@hamur/react/segment-group'

const views = [
  { value: 'day', label: 'Day' },
  { value: 'week', label: 'Week' },
  { value: 'month', label: 'Month' },
]

export default function SegmentGroupExample() {
  return (
    <div>
      <h3 id="segment-group-view" className="segment-group-heading">
        View
      </h3>
      <SegmentGroup.Root
        className="segment-group"
        aria-labelledby="segment-group-view"
        defaultValue="week"
      >
        <SegmentGroup.Indicator className="segment-group-indicator" />
        {views.map((view) => (
          <SegmentGroup.Item
            key={view.value}
            className="segment-group-item"
            value={view.value}
          >
            {view.label}
          </SegmentGroup.Item>
        ))}
      </SegmentGroup.Root>
    </div>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<SegmentGroup.Root>
  <SegmentGroup.Indicator />
  <SegmentGroup.Item />
</SegmentGroup.Root>

Examples

Vertical

A row density choice stacked in a column; the pill moves on the block axis.

SegmentGroupVertical.tsx
import { SegmentGroup } from '@hamur/react/segment-group'

const densities = [
  { value: 'compact', label: 'Compact' },
  { value: 'comfortable', label: 'Comfortable' },
  { value: 'spacious', label: 'Spacious' },
]

export default function SegmentGroupVerticalExample() {
  return (
    <SegmentGroup.Root
      className="segment-group"
      aria-label="Row density"
      orientation="vertical"
      defaultValue="comfortable"
    >
      <SegmentGroup.Indicator className="segment-group-indicator" />
      {densities.map((density) => (
        <SegmentGroup.Item key={density.value} className="segment-group-item" value={density.value}>
          {density.label}
        </SegmentGroup.Item>
      ))}
    </SegmentGroup.Root>
  )
}

Disabled

A billing period whose third segment is disabled; the arrow keys skip it.

SegmentGroupDisabled.tsx
import { SegmentGroup } from '@hamur/react/segment-group'

export default function SegmentGroupDisabledExample() {
  return (
    <div className="stack">
      <SegmentGroup.Root className="segment-group" aria-label="Billing" defaultValue="monthly">
        <SegmentGroup.Indicator className="segment-group-indicator" />
        <SegmentGroup.Item className="segment-group-item" value="monthly">
          Monthly
        </SegmentGroup.Item>
        <SegmentGroup.Item className="segment-group-item" value="yearly">
          Yearly
        </SegmentGroup.Item>
        <SegmentGroup.Item className="segment-group-item" value="lifetime" disabled>
          Lifetime
        </SegmentGroup.Item>
      </SegmentGroup.Root>
      <p className="muted">Lifetime is sold out.</p>
    </div>
  )
}

Controlled

Icon-only alignment segments named by aria-label, with the value held by the app and a Reset button.

SegmentGroupControlled.tsx
import * as React from 'react'
import { SegmentGroup } from '@hamur/react/segment-group'

type Align = 'left' | 'center' | 'right'

const alignments: { value: Align; label: string; lines: string[] }[] = [
  { value: 'left', label: 'Align left', lines: ['M4 6h16', 'M4 12h10', 'M4 18h14'] },
  { value: 'center', label: 'Align center', lines: ['M4 6h16', 'M7 12h10', 'M5 18h14'] },
  { value: 'right', label: 'Align right', lines: ['M4 6h16', 'M10 12h10', 'M6 18h14'] },
]

export default function SegmentGroupControlledExample() {
  const [align, setAlign] = React.useState<Align>('left')
  return (
    <div className="stack">
      <div className="row">
        <SegmentGroup.Root
          className="segment-group"
          aria-label="Text alignment"
          value={align}
          onValueChange={setAlign}
        >
          <SegmentGroup.Indicator className="segment-group-indicator" />
          {alignments.map((alignment) => (
            <SegmentGroup.Item
              key={alignment.value}
              className="segment-group-item segment-group-icon"
              value={alignment.value}
              aria-label={alignment.label}
            >
              <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
                {alignment.lines.map((d) => (
                  <path key={d} d={d} />
                ))}
              </svg>
            </SegmentGroup.Item>
          ))}
        </SegmentGroup.Root>
        <button
          type="button"
          className="button segment-group-reset"
          onClick={() => setAlign('left')}
        >
          Reset
        </button>
      </div>
      <p className="segment-group-preview" style={{ textAlign: align }}>
        The quick brown fox jumps over the lazy dog.
      </p>
      <p className="muted segment-group-value">Alignment: {align}</p>
    </div>
  )
}

Form

In a Field labelled "Default view", submitted with its form and brought back to Week by a reset.

SegmentGroupForm.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { SegmentGroup } from '@hamur/react/segment-group'

const views = [
  { value: 'day', label: 'Day' },
  { value: 'week', label: 'Week' },
  { value: 'month', label: 'Month' },
]

export default function SegmentGroupFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="view" className="field">
        <Field.Label className="segment-group-label" nativeLabel={false} render={<div />}>
          Default view
        </Field.Label>
        <SegmentGroup.Root className="segment-group" defaultValue="week">
          <SegmentGroup.Indicator className="segment-group-indicator" />
          {views.map((view) => (
            <SegmentGroup.Item key={view.value} className="segment-group-item" value={view.value}>
              {view.label}
            </SegmentGroup.Item>
          ))}
        </SegmentGroup.Root>
      </Field.Root>
      <div className="row">
        <button className="button" type="submit">
          Save
        </button>
        <button className="button" type="reset" onClick={() => setSubmitted('')}>
          Reset
        </button>
      </div>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

API reference

17 props across 3 parts, in React syntax. className, style, render are listed once, under Shared props.

IndicatorSegmentGroup.Indicator

A visual indicator that can be styled to match the position of the checked segment. Renders a <span>.

Indicator props
PropTypeDefault
renderBeforeHydrationWhether to render itself before React hydrates.booleanfalse
Indicator data attributes
Data attributeDescription
data-orientationIndicates the layout direction of the segments.
data-activation-directionIndicates the direction of the last move (based on the previously checked segment).
data-resizingPresent from a re-measure that is not a change of value (a resize) until the next animation frame, so a transition can be turned off while it is present.
Indicator CSS variables
CSS variableDescription
--active-segment-leftThe distance from the left edge of the Root's padding box to the checked segment.
--active-segment-rightThe distance from the checked segment to the right edge of the Root's padding box.
--active-segment-topThe distance from the top edge of the Root's padding box to the checked segment.
--active-segment-bottomThe distance from the checked segment to the bottom edge of the Root's padding box.
--active-segment-widthThe width of the checked segment.
--active-segment-heightThe height of the checked segment.

ItemSegmentGroup.Item

A segment of the group: a radio with its text as its name. and a hidden <input> beside. Renders a <span>.

Item props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
valuerequiredThe unique identifying value of the segment in the group.TValue—
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to check the segment.booleanfalse
requiredWhether the user must choose a value before submitting a form.booleanfalse
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
Item data attributes
Data attributeDescription
data-orientationIndicates the layout direction of the segments.
data-checkedPresent when the segment is checked.
data-uncheckedPresent when the segment is not checked.
data-disabledPresent when the segment is disabled.
data-readonlyPresent when the segment is readonly.
data-requiredPresent when the segment is required.
data-validPresent when the segment is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the segment is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the segment has been touched (when wrapped in Field.Root).
data-dirtyPresent when the segment's value has changed (when wrapped in Field.Root).
data-filledPresent when the segment is checked (when wrapped in Field.Root).
data-focusedPresent when the segment is focused (when wrapped in Field.Root).

RootSegmentGroup.Root

Groups the segments of a single-choice bar and measures the checked one for the Indicator. Renders a <div>.

Root props
PropTypeDefault
valueThe controlled value of the checked segment, or null for none.TValue | null—
defaultValueThe uncontrolled value of the segment checked when the component is initially rendered, andTValue | null—
onValueChangeCallback fired when the value changes.((value: TValue, eventDetails: { reason: "none"; event: Event; cancel: () => void; allowPropagation: () => void; isCanceled: boolean; isPropagationAllowed: boolean; trigger: Element | undefined; }) => void)—
orientationThe layout direction of the segments.SegmentGroupOrientation'horizontal'
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to check a different segment.booleanfalse
requiredWhether the user must choose a value before submitting a form.booleanfalse
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden inputs.string—
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
Root data attributes
Data attributeDescription
data-orientationIndicates the layout direction of the segments.
data-disabledPresent when the segment group is disabled.
data-validPresent when the segment group is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the segment group is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the segment group has been touched (when wrapped in Field.Root).
data-dirtyPresent when the segment group's value has changed (when wrapped in Field.Root).
data-filledPresent when a segment is checked (when wrapped in Field.Root).
data-focusedPresent when a segment is focused (when wrapped in Field.Root).

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowLeftArrowRightArrowUpArrowDownMoves to the previous or next segment and checks it, wrapping at the ends, in either layoutItem
ArrowRightSkips a disabled segmentItem
SpaceChecks the focused segmentItem
EnterChanges nothing and submits nothingItem
HomeEndChange nothingItem
TabEnters the group on the checked segment, then leaves it in one stepRoot

Accessibility

What the parts set
PartSetsSo that
Rootrole="radiogroup" and aria-orientation, named by the Field label, a Fieldset legend, or the app's aria-labelledby or aria-labelthe segments are said as one set, with its question
Rootaria-disabled, aria-readonly, aria-required from disabled, readOnly and requiredthe group's constraints are said
Itemrole="radio" with aria-checked, the roving tabindex, and a hidden radio input beside iteach segment is said as a choice with its state, and submits a value
Itemits text as its name; an icon-only Item takes the app's aria-labelevery segment has a name
Indicatorrole="presentation", no text, never focused; hidden until measuredthe pill is drawing only, and nothing jumps on the first paint

Focus order

  1. The group is one tab stop: the checked segment, or the first enabled one while none is checked.
  2. The arrow keys move focus and the choice together; disabled segments are skipped.
  3. Tab leaves the group in one step. Nothing takes focus on mount or on a form reset.