Checkbox and radio

Free · MITReact Native · Expo

Checkboxes whose check draws itself in (an indeterminate dash retracts into it) and radios whose dot pops into the center, inline or as list rows, with CheckboxGroup and RadioGroup helpers.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/checkbox.json

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/checkbox.

Usage

import { useState } from "react";
import { Checkbox, RadioGroup } from "@/components/goodui-native/checkbox/checkbox";

export function Checkout() {
  const [speed, setSpeed] = useState<"standard" | "express">("standard");
  const [terms, setTerms] = useState(false);
  return (
    <>
      <RadioGroup
        accessibilityLabel="Delivery"
        value={speed}
        onValueChange={setSpeed}
        options={[
          { value: "standard", label: "Standard", description: "3 to 5 days, free" },
          { value: "express", label: "Express", description: "Tomorrow, $9.90" },
        ]}
      />
      <Checkbox label="I accept the terms of service" checked={terms} onCheckedChange={setTerms} />
    </>
  );
}

When to use

Use it for
  • Several independent choices in a form or filter: amenities, toppings, notification types (CheckboxGroup).
  • One choice out of a few that should all be visible: delivery speed, plan, payment method (RadioGroup).
  • Accepting terms before continuing (Checkbox with invalid when skipped).
  • A select-all checkbox that shows a partial selection (checked='indeterminate').
Reach for something else
  • A setting that takes effect immediately: use switch.
  • Two to five views of the same data: use segmented-control.
  • Long option lists: use a searchable list with search-bar.

Examples

Select all with indeterminate

const all = picked.length === items.length;
<Checkbox
  label="Select all"
  checked={all ? true : picked.length ? "indeterminate" : false}
  onCheckedChange={(on) => setPicked(on ? items.map((i) => i.id) : [])}
/>

Settings-style rows with trailing checkboxes

<CheckboxGroup
  position="trailing"
  value={channels}
  onValueChange={setChannels}
  options={[
    { value: "email", label: "Email" },
    { value: "sms", label: "Text message" },
    { value: "push", label: "Push" },
  ]}
/>

API reference

Checkbox

PropTypeDefault
label
Visible label. The whole label area toggles.
string—
description
Secondary line under the label.
string—
variant
`inline` hugs its content; `row` is a full-width list row for settings and grouped lists.
"row" | "inline"—
position
Where the control sits. Default `leading`; `trailing` matches iOS settings lists.
"leading" | "trailing"—
size
Box 18 (`sm`) or 22 pt (`md`). The touch target stays 44 pt.
"sm" | "md"—
tone
Fill color when selected. `accent` by default.
"success" | "danger" | "accent"—
invalid
Red border, for a required choice that was skipped (for example terms not accepted).
boolean—
disabled
Dims the control and ignores presses.
booleanfalse
haptic
`false` silences the select haptic.
booleantrue
accessibilityLabel
Screen reader label when there is no visible `label`.
string—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—
checked
`true`, `false` or `"indeterminate"` (controlled). Pair it with `onCheckedChange`.
false | true | "indeterminate"—
defaultChecked
Initial state when uncontrolled.
false | true | "indeterminate"false
onCheckedChange
Called with the new state. Pressing an indeterminate box checks it.
(checked: boolean) => void—

Radio

PropTypeDefault
label
Visible label. The whole label area toggles.
string—
description
Secondary line under the label.
string—
variant
`inline` hugs its content; `row` is a full-width list row for settings and grouped lists.
"row" | "inline"—
position
Where the control sits. Default `leading`; `trailing` matches iOS settings lists.
"leading" | "trailing"—
size
Box 18 (`sm`) or 22 pt (`md`). The touch target stays 44 pt.
"sm" | "md"—
tone
Fill color when selected. `accent` by default.
"success" | "danger" | "accent"—
invalid
Red border, for a required choice that was skipped (for example terms not accepted).
boolean—
disabled
Dims the control and ignores presses.
booleanfalse
haptic
`false` silences the select haptic.
booleantrue
accessibilityLabel
Screen reader label when there is no visible `label`.
string—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—
selected
Selected (controlled). A radio is only ever selected by pressing it, never unselected.
booleanfalse
onSelect
Called when the radio is pressed while not selected.
() => void—

CheckboxGroup

PropTypeDefault
options*
The choices.
ChoiceOption<T>[]—
variant
`row` (default) draws full-width rows separated by hairlines; `inline` stacks compact items.
"row" | "inline""row"
position
Where each control sits.
"leading" | "trailing"—
size"sm" | "md"—
tone"success" | "danger" | "accent"—
disabled
Dims the control and ignores presses.
boolean—
haptic
`false` silences the select haptics.
boolean—
accessibilityLabel
Screen reader name of the group, for example "Delivery speed".
string—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—
value
Checked values (controlled).
T[]—
defaultValue
Initially checked values when uncontrolled.
T[][]
onValueChange
Called with the new list of checked values, in option order.
(value: T[]) => void—

RadioGroup

PropTypeDefault
options*
The choices.
ChoiceOption<T>[]—
variant
`row` (default) draws full-width rows separated by hairlines; `inline` stacks compact items.
"row" | "inline""row"
position
Where each control sits.
"leading" | "trailing"—
size"sm" | "md"—
tone"success" | "danger" | "accent"—
disabled
Dims the control and ignores presses.
boolean—
haptic
`false` silences the select haptics.
boolean—
accessibilityLabel
Screen reader name of the group, for example "Delivery speed".
string—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—
value
Selected value (controlled).
T—
defaultValue
Initially selected value when uncontrolled.
T—
onValueChange
Called with the newly selected value.
(value: T) => void—

Motion and haptics

  • Check: the fill closes in from 40 % on the snappy spring, then the check stroke draws itself in 240 ms.
  • Checked to indeterminate (and back): one stroke retracts while the other draws.
  • Radio: the fill closes in and the center dot pops.
  • Press: the box scales to 0.88 from touch down; rows also fill with a muted background.
  • Reduced motion: no scaling; fill, stroke and dot appear with a short fade-speed timing.
  • Haptic: pop when a checkbox is checked, tick when it is unchecked or a radio is chosen (a radio that is already selected does nothing)
  • The stroke is drawn with an animated strokeDashoffset on the UI thread; a change is one React update.

Accessibility

  • Each item is a checkbox or radio (accessibilityRole) whose label is 'label, description'. Checkboxes report checked true, false or mixed; radios report checked and selected.
  • RadioGroup is a radiogroup labelled by accessibilityLabel.
  • Without a visible label pass accessibilityLabel; the bare control then gets hitSlop up to 44 pt. Labelled items are at least 44 pt tall.

Notes for AI

  • Checkbox: checked + onCheckedChange (or defaultChecked). onCheckedChange always receives a boolean; pressing an indeterminate box checks it.
  • Radio has no unselect. Prefer RadioGroup with options over wiring Radio by hand.
  • CheckboxGroup returns values in option order, not tap order.
  • variant='row' (the group default) is for lists on a surface without padding; variant='inline' for compact forms.
  • Use invalid to mark a required checkbox that was skipped, with the reason in text near it.