Skip to content
kerfjsGitHub

RadioGroup

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

SourcePackage @kerf-js/veneer

Preview

tsx
import { state } from '@kerf-js/core'import { RadioGroup } from '@kerf-js/veneer'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 @kerf-js/veneer

Usage

tsx
import { RadioGroup } from '@kerf-js/veneer'

Labels only

Leave out hint when the label says enough.

tsx
import { state } from '@kerf-js/core'import { RadioGroup } from '@kerf-js/veneer'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 '@kerf-js/core'import { Badge, RadioGroup, Row } from '@kerf-js/veneer'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 '@kerf-js/core'import {  RadioGroup,  Row,  Slider,  Stack,  Text} from '@kerf-js/veneer'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 '@kerf-js/core'import {  Button,  RadioGroup,  Row,  Stack,  Text} from '@kerf-js/veneer'export function Form() {  const digest = state('daily')  const sent = state('')  const save = (e: Event) => {    e.preventDefault()    const form = new FormData(      e.currentTarget as HTMLFormElement,    )    const value = form.get('digest') as string | null    sent.value = `digest=${value ?? ''}`  }  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