Checkbox and radio
Free · MITReact Native · ExpoCheckboxes 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/checkbox.jsonThen 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
- 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').
- 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
| Prop | Type | Default |
|---|---|---|
| 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. | boolean | false |
| haptic `false` silences the select haptic. | boolean | true |
| 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
| Prop | Type | Default |
|---|---|---|
| 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. | boolean | false |
| haptic `false` silences the select haptic. | boolean | true |
| 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. | boolean | false |
| onSelect Called when the radio is pressed while not selected. | () => void | — |
CheckboxGroup
| Prop | Type | Default |
|---|---|---|
| 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
| Prop | Type | Default |
|---|---|---|
| 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.
Related
An iOS-style switch with squash and stretch: the thumb stretches toward where it is going on touch down, then travels on a spring while the track color follows. Settings-row layout, tones, sizes and optimistic async changes.
Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.
A text field whose label glides up as you focus it, with helper and error text that slide in, a shake and haptic on error, clear and reveal buttons, a character counter and a multiline mode that grows with its text.