How to Add a Checkbox to a React App
Step-by-step guide to adding a copy-and-own Checkbox to any React app with the Drivn CLI — labels, controlled state, disabled, and native form support.
A checkbox is the simplest form control with a deceptively long tail — it has a checked and unchecked state, a clickable label, a keyboard focus ring, and an accessible name that screen readers announce. The native <input type="checkbox"> handles all of that behavior but is notoriously hard to style consistently across browsers, which is why most design systems hide the native box and paint their own on top.
Drivn does exactly that, then writes the result into your repository instead of shipping a runtime package. The CLI copies a checkbox.tsx file: a React.forwardRef input that renders a visually hidden native checkbox for behavior and accessibility, plus a styled <span> box that shows a lucide-react Check icon when selected. The whole thing is about 50 lines of TSX with lucide-react and the local cn utility as its only dependencies — no Radix, no class-variance-authority. Because it uses useState for its uncontrolled mode, the source is marked 'use client', while the page around it can still render on the server.
This guide adds the Checkbox to an existing React app — install the CLI, render it with a label, wire controlled state through onChange, then disable it and drop it into a form. It works the same in Vite + React or a Next.js App Router project. For the full reference see the Checkbox docs; for live patterns see the Checkbox examples.
Prerequisites
Before installing the Checkbox, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. For a custom setup, check the compilerOptions.paths entry in tsconfig.json; the installation page lists the minimal config. The Checkbox has the lightest dependency surface of any Drivn component — it pulls in lucide-react for the single Check icon and nothing else, since the box, the border, and the checked fill are all Tailwind classes reading from your theme tokens.
Step 1 — Install Drivn via the CLI
Run the CLI from your project root to add the Checkbox source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes checkbox.tsx, and adds lucide-react to your package.json if it is not already present. No global config file is created — the Checkbox is a single TypeScript file in your repo that you edit like any other component. Confirm the file landed in your editor, then commit it. There are no peer components to resolve: unlike the Date Picker or Command, the Checkbox stands alone. If your project uses a monorepo layout or a non-standard path, the CLI docs cover the flags for targeting a custom location during install.
1 # add the Checkbox to your existing React project 2 npx drivn add checkbox 3 4 # verify the file was written 5 ls src/components/ui/checkbox.tsx
Step 2 — Render with a label
Import the Checkbox from your UI directory and render it with a label prop — that is the entire setup for an uncontrolled checkbox. The component wraps the native input and your label text in a single <label> element, so clicking the text toggles the box for free; you never wire htmlFor and id by hand. Without a checked prop the component manages its own state internally via useState(defaultChecked ?? false), so it works in a plain form with no React state at all. Pass defaultChecked to start it ticked. Because the source is 'use client', render it inside a client component in a Next.js App Router app. See the Checkbox docs for the full prop table.
1 import { Checkbox } from '@/components/ui/checkbox' 2 3 export function Preferences() { 4 return ( 5 <div className="flex flex-col gap-3"> 6 <Checkbox label="Email notifications" defaultChecked /> 7 <Checkbox label="SMS notifications" /> 8 <Checkbox label="Push notifications" /> 9 </div> 10 ) 11 }
Step 3 — Controlled state and onChange
To drive the Checkbox from React state, pass both checked and onChange. The source detects controlled mode with const isControlled = checked !== undefined and, when controlled, skips its internal setState and renders whatever boolean you pass. The onChange handler receives the native input change event, so read the next value from e.target.checked and store it. Controlled mode is what you want whenever the checked state drives other UI — a "select all" master checkbox, a submit button gated on a terms box, or a value synced into a form library like react-hook-form. Hold the boolean in useState and bind both props, as below.
1 'use client' 2 import { useState } from 'react' 3 import { Checkbox } from '@/components/ui/checkbox' 4 5 export function TermsGate() { 6 const [accepted, setAccepted] = useState(false) 7 8 return ( 9 <div className="flex flex-col gap-3"> 10 <Checkbox 11 label="I accept the terms and conditions" 12 checked={accepted} 13 onChange={(e) => setAccepted(e.target.checked)} 14 /> 15 <button disabled={!accepted}>Continue</button> 16 </div> 17 ) 18 }
Step 4 — Disabled, custom styles, and forms
Three finishing touches cover most real forms. Pass disabled to dim the control with opacity-50 cursor-default and block interaction — the native input is disabled too, so it is skipped during submission and keyboard tabbing. Because the Checkbox spreads ...props onto the underlying <input>, every standard attribute works: give it a name and value and it submits with a native <form> exactly like any checkbox, or add required for built-in validation. To restyle, open src/components/ui/checkbox.tsx and edit the styles object — box controls the unchecked square (w-4 h-4 rounded-[4px] border border-border) and checked controls the filled state (bg-primary border-primary). Every class reads from your Tailwind tokens, so a theme change in globals re-themes every Checkbox at once. See the Checkbox examples for grouped lists and form layouts.
1 import { Checkbox } from '@/components/ui/checkbox' 2 3 export function NewsletterForm() { 4 return ( 5 <form className="flex flex-col gap-3"> 6 <Checkbox 7 name="newsletter" 8 value="weekly" 9 label="Send me the weekly newsletter" 10 required 11 /> 12 <Checkbox label="Already unsubscribed" checked disabled /> 13 </form> 14 ) 15 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
Frequently asked questions
Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add checkbox. The Checkbox has no dependency on Next.js or any router — it renders anywhere React and Tailwind reach the DOM, and it composes no other component, so the CLI writes a single self-contained file. Vite + React is the most common non-Next setup and needs no extra configuration.
Pass both checked and onChange. The source sets isControlled = checked !== undefined, and in controlled mode it skips its internal useState update and renders the boolean you supply. The onChange handler receives the native input change event, so read e.target.checked for the next value and store it in your own state. Omit checked to let the component manage itself with defaultChecked as the starting value.
Yes to both. The component renders the native input and the label text inside one <label> element, so clicking anywhere on the row toggles the box without you wiring htmlFor and id. The real <input type="checkbox"> is kept in the DOM with an sr-only class rather than removed, which preserves keyboard focus, the space-bar toggle, and the accessible name that screen readers announce.
Yes. The Checkbox spreads ...props onto the underlying native <input>, so standard form attributes pass straight through — give it a name and value and it serializes into FormData on submit, or add required for native validation. The visually hidden input is a real checkbox, not a styled <div>, so a plain <form> and Next.js Server Actions read it without any extra adapter.

