Checkbox React component for Tailwind CSS v4
v1.0.1Updated in 1.9.0
A React checkbox that is on, off or mixed, with a check that draws itself, full-row labels, cards, and groups with select all.
Click the box, its label or its description and the check draws itself on a quick spring; uncheck it and the stroke pulls back faster. While you press, the box dips a little, and with a mouse its border darkens on hover. Tab reaches every box and Space toggles it (Enter does not, as the ARIA pattern says); an indeterminate box turns checked on the next press. A link inside the label opens without toggling. Press Place order with the terms unchecked and the error slides in under the row; checking the box clears it. Cards take a ring in the tone when checked and dip slightly while pressed. In a group, Select all checks every enabled option and shows a dash while only some are checked (it turns checked once every enabled option is); with a maximum, the remaining options disable once you reach it. On a phone every row is at least 44px tall and the whole row is the target, and with reduced motion the check appears without drawing and nothing dips.
Checkbox is a free React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add checkbox, together with Field, useControllableState, Motion presets, Tones and UIProvider and the npm packages @radix-ui/react-checkbox and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add checkboxUsage
Import Checkbox from @/components/ui/checkbox.
import { useState } from "react"
import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox"
const AMENITIES = [
{ value: "parking", label: "Parking" },
{ value: "balcony", label: "Balcony" },
{ value: "elevator", label: "Elevator" },
{ value: "laundry", label: "In-unit laundry" },
]
export function OrderConsent({ showError }: { showError: boolean }) {
const [accepted, setAccepted] = useState(false)
const [amenities, setAmenities] = useState<string[]>(["parking"])
return (
<form className="flex flex-col gap-4">
<Checkbox
name="terms"
required
checked={accepted}
onCheckedChange={(checked) => setAccepted(checked === true)}
label="I agree to the Terms and conditions"
error={showError && !accepted ? "You need to accept the terms." : undefined}
/>
<CheckboxGroup
label="Amenities"
name="amenities"
columns={2}
options={AMENITIES}
value={amenities}
onValueChange={setAmenities}
/>
</form>
)
}Props
labeldefault""ReactNodeText beside the box. The whole row is one <label>, so clicking the text, the description or the empty space toggles the box. A link inside it opens without toggling.
descriptiondefault""ReactNodeA second line under the label, in the muted caption style. It is kept out of the box's name and linked with aria-describedby, so screen readers read it after the label.
errordefault""ReactNodeA message under the row, lined up with the label, that slides in and marks the box invalid (red border, aria-invalid). "", false and null are no error. A box without a label only turns red; wrap it in a Field to show the text.
requireddefaultfalsebooleanShows the red mark after the label, sets aria-required, and inside a native form blocks submission while unchecked (through Radix's hidden input).
invaliddefaultfalsebooleanThe red border and aria-invalid without a message, for forms that show errors elsewhere. Inherits from a parent Field.
disableddefaultfalsebooleanDims the row to 50% and blocks it. A disabled Field or FieldGroup around it disables it too.
readOnlydefaultfalsebooleanShows the value but ignores clicks and Space, with no hover or press feedback; the box keeps focus and sets aria-readonly.
defaultCheckeddefaultfalseboolean | "indeterminate"The starting state while uncontrolled: true, false or "indeterminate" (a dash, aria-checked="mixed").
checkedboolean | "indeterminate"The state while controlled. Pair it with onCheckedChange.
onCheckedChange(checked: boolean | "indeterminate") => voidCalled with the next state. Clicking an indeterminate box makes it true, like Radix.
sizedefault"md""sm" | "md" | "lg"Box 16 / 18 / 20px with a 12 / 14 / 16px check and a 13 / 14 / 15px label. The box centers on the label's first line at every size.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The checked fill (and the card ring and icon tile). neutral is black in light mode and white in dark mode; keep brand for the one place it means something.
colordefault""stringAny CSS color. Replaces the tone; the check turns black or white by the color's lightness. Leave empty to use the tone.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Box corners, scaled with the size: md is 4 / 5 / 6px, full makes a round checkbox.
variantdefault"default""default" | "card"card turns the row into a soft-filled tile with the box at the top end, an optional icon tile, and a ring in the tone when checked. For rich choices like paid extras.
labelPositiondefault"end""end" | "start"start puts the label first and the box at the end of a full-width row, like a settings list. Cards ignore it.
iconReactNodeCards: the leading icon in a 40px tile that takes the tone when checked. Rows: a thumbnail or avatar between the box and the text (the row then centers).
namedefault""stringForm field name. Inside a <form> Radix renders a hidden input that submits value while checked and returns to its first state when the form resets. The playground sits in a real form.
valuedefault"on"stringWhat the form submits while the box is checked.
idstringThe box's id. Defaults to useId(); inside a Field the Field's id wins, so its label toggles the box.
classNamesPartial<Record<"field" | "box" | "indicator" | "content" | "label" | "description" | "error" | "icon", string>>Extra classes for the parts: field is the label row (or the card), box the button, indicator the check svg. Padding for list rows goes on field so the padding stays clickable.
classNamestringClasses for the outermost element: the wrapper around the row and its message, or the box itself when there is no label. ref and every other prop (aria-*, data-*, onBlur, form) go to the box, a <button role="checkbox">, so form libraries can focus it.
Parts
CheckboxGroup
componentA list of options with one label (role="group"). options: { value, label, description?, icon?, disabled? }[]; value / defaultValue / onValueChange (string[], kept in the options' order); label, description, error, required, invalid, disabled, readOnly; orientation ("vertical" | "horizontal", rows wrap); columns (1 | 2 | 3 | 4, one on phones); variant, size, tone, color, radius, labelPosition for every box; selectAll and selectAllLabel ("Select all"); max (0 = no limit) with maxLabel (n => `Choose up to ${n}`); name (every checked box submits its value, FormData.getAll(name)); fieldProps; classNames (group, all, option, box, indicator, icon, content, optionLabel, optionDescription plus the Field parts). Props: CheckboxGroupProps.
CheckboxIndicator
componentThe box alone, aria-hidden, with the same look and animation but no behavior: checked, size, tone, color, radius, invalid, disabled. For listbox rows (role="option" must not hold a real checkbox) and table cells that own the state. Props: CheckboxIndicatorProps.
CheckboxOption
typeOne option of CheckboxGroup: { value, label, description?, icon?, disabled? }.
Examples
Group in two columns
CheckboxGroup with a label and a name: every checked box submits its value, shown below as form data.
Cards with prices
variant="card" with icon tiles in the brand tone. The ring and the tile take the tone; the total follows in dollars.
Select all
selectAll adds a row that shows a dash while some are checked. Rows with a thumbnail center; delete the selection to see the list update.
Maximum choices
max={3}: once three are checked the rest disable, and maxLabel explains why.
Sizes, tones and radius
16, 18 and 20px boxes, every tone, a custom color, round and square corners, and the aria-hidden CheckboxIndicator for listbox rows.
Settings list
labelPosition="start" rows inside a list group: the whole row is the target, padding goes on classNames.field.
Disabled, read-only and invalid
Dimmed, locked and red states; the last one shows its error until you check it.
Files
- components/ui/checkbox.tsx
FAQ
Is the React Checkbox component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Checkbox?
Run npx wingo-ui@latest add checkbox. It also adds the Wingo UI items it builds on (Field, useControllableState, Motion presets, Tones and UIProvider) and the npm packages @radix-ui/react-checkbox and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/checkbox.json.
How does Checkbox behave on phones?
On a phone every row is at least 44px tall and the whole row is the target, and with reduced motion the check appears without drawing and nothing dips.
See also
Used in
More React input components
Articles using this component
- React Multi Select: Search, Chips, Select All and Mobile
Component guides10 min read
- Minimum Touch Target Size: WCAG, iOS, Android and Tailwind
Mobile UI in React11 min read
- React Form Components: Accessible Fields, Zod and Mobile
Component guides26 min read