Skip to content
weftGitHub

RadioGroup

Pick exactly one from a short list where every option is on show.

SourcePackage @weft-js/warp

Preview

tsx
import { state } from '@weft-js/core'import { RadioGroup } from '@weft-js/warp'export function Window() {  const when = state<'now' | 'night' | 'weekend'>('now')  return <RadioGroup label="Maintenance window" value={when.value} options={[...WINDOWS]} onChange={v => (when.value = v)} />}const WINDOWS = [  { value: 'now', label: 'Right away', hint: 'Starts within a minute.' },  { value: 'night', label: 'Tonight', hint: 'At 02:00 in the project\'s region.' },  { value: 'weekend', label: 'This weekend', hint: 'Saturday at 02:00.', disabled: true },] as const

Installation

npm install @weft-js/warp

Usage

tsx
import { RadioGroup } from '@weft-js/warp'

Labels only

Leave out hint when the label says enough.

tsx
import { state } from '@weft-js/core'import { RadioGroup } from '@weft-js/warp'export function Plain() {  const branch = state('main')  return <RadioGroup label="Deploy from" value={branch.value} options={[{ value: 'main', label: 'main' }, { value: 'release', label: 'release' }, { value: 'tags', label: 'Tagged commits only' }]} onChange={v => (branch.value = v)} />}

More than words

A label can be any content, like a badge with each region's latency.

tsx
import { state } from '@weft-js/core'import { Badge, RadioGroup, Row } from '@weft-js/warp'export function Rich() {  const region = state('eu-west')  return <RadioGroup label="Region" value={region.value} options={NEAR} onChange={v => (region.value = v)} />}const LATENCY = { 'eu-west': 18, 'us-east': 94, 'ap-south': 162 } as Record<string, number>const NEAR = Object.keys(LATENCY).map(r => ({  value: r,  label: <Row gap="related">{r}<Badge tone={LATENCY[r]! < 50 ? 'positive' : LATENCY[r]! < 120 ? 'caution' : 'critical'}>{LATENCY[r]} ms</Badge></Row>,  hint: r === 'eu-west' ? 'Closest to you.' : undefined,}))

Choose a plan

Plan and seats both change the total.

tsx
import { state } from '@weft-js/core'import { RadioGroup, Row, Slider, Stack, Text } from '@weft-js/warp'export function Plan() {  const plan = state('team')  const seats = state(8)  return (    <Stack gap="group">      <RadioGroup label="Plan" value={plan.value} options={PLANS} onChange={v => (plan.value = v)} />      <Slider label="Seats" value={seats.value} min={1} max={40} onChange={v => (seats.value = v)} />      <Row justify="between">        <Text subtle>Per month</Text>        <Text role="heading">€{SEAT[plan.value]! * seats.value}</Text>      </Row>    </Stack>  )}const min = 60_000const PLANS = [  { value: 'hobby', label: 'Hobby', hint: 'One project, free.' },  { value: 'team', label: 'Team', hint: '€20 a seat, unlimited projects.' },  { value: 'scale', label: 'Scale', hint: '€60 a seat, plus private regions.' },]const SEAT = { hobby: 0, team: 20, scale: 60 } as Record<string, number>

In a form

name is what the choice posts under. Save reads it back from the form's data.

tsx
import { state } from '@weft-js/core'import { Button, RadioGroup, Row, Stack, Text } from '@weft-js/warp'export function Form() {  const digest = state('daily')  const sent = state('')  const save = (e: Event) => {    e.preventDefault()    sent.value = `digest=${(new FormData(e.currentTarget as HTMLFormElement).get('digest') as string | null) ?? ''}`  }  return (    <Stack as="form" gap="group" onSubmit={save}>      <Text role="heading">Failure emails</Text>      <RadioGroup label="Failure emails" name="digest" value={digest.value} options={DIGESTS} onChange={v => (digest.value = v)} />      <Row justify="between">        <Text subtle role="caption">{sent.value ? `Posted ${sent.value}` : 'Nothing saved yet'}</Text>        <Button type="submit">Save</Button>      </Row>    </Stack>  )}const DIGESTS = [  { value: 'instant', label: 'As it happens', hint: 'One email for each failed deploy.' },  { value: 'daily', label: 'Daily digest', hint: 'At 08:00, if anything failed.' },  { value: 'off', label: 'Off', hint: 'Check the dashboard yourself.' },]

When to use

Use it for

  • Two to five options people should read and compare

Reach for something else

  • Select, when there are many options
  • Segmented, when the options are one or two words and switch a view

Accessibility

↑↓
Moves to the next choice and picks it
Tab
Moves in and out of the group
  • label names the group for screen readers, but isn't drawn. Put a heading above it when people need one too.

API Reference

PropTypeDefaultDescription
labelstring—The group's accessible name.
valueV—The value that is picked.
optionsRadioOption<V>[]—The choices: { value, label, hint, disabled }.
namestring—The form field name. Made up for you when left out.
EventTypeDefaultDescription
onChange(v: V) => void—Called with the new value.

RelatedRadioSelectSegmented