Next.js Checkbox — Forms, Server Actions & FormData
Use the Drivn Checkbox in Next.js 16 — submit boolean fields to a Server Action via FormData, control state on the client, and keep the native input accessible.
A checkbox looks trivial until you wire it into a Next.js form and have to decide where the server-client boundary sits. The Drivn Checkbox is one of the components that carries a 'use client' directive, because it tracks its own state with useState for the uncontrolled case. That single line is the whole story of how it fits into the App Router: anywhere you render an interactive checkbox, you are on the client side of the boundary.
What makes the component fit Server Actions cleanly is a detail in its markup. The real <input type="checkbox"> is rendered with className="sr-only", not removed from the DOM — the styled box you see is a sibling <span>. Because the native input is only visually hidden, it stays in the accessibility tree and, more importantly, it stays in the form. Give the Checkbox a name and it submits its value through FormData to a Server Action exactly like a plain HTML checkbox, with no onChange plumbing at all.
This guide installs the Checkbox in a Next.js 16 project, submits a boolean field to a Server Action with FormData, controls the checked state from React when you need live UI, and covers the disabled and accessibility behavior. Every snippet matches the source the Drivn CLI writes into your repo. For the full prop table see the Checkbox docs; for the head-to-head see Drivn vs shadcn/ui Checkbox.
Install the Checkbox in a Next.js 16 project
Drivn ships no runtime package — the CLI copies the component source into your repository and leaves. From the root of your Next.js 16 project run npx drivn add checkbox. The CLI asks once where components live, defaulting to src/components/ui/, and writes checkbox.tsx there. The file imports React, the local cn class-merge helper, and a single icon — Check from lucide-react for the tick mark — so confirm that package is present. Everything else is Tailwind classes collected in a styles object at the top of the file. The CLI reference covers custom directories and batch installs, and the installation guide lists project prerequisites. Once the file lands it is yours to edit; Drivn releases never overwrite it.
1 # from the root of your Next.js 16 project 2 npx drivn add checkbox
Why the Checkbox needs 'use client'
Open checkbox.tsx and the first line is 'use client'. The reason is the uncontrolled path: the component initializes const [internal, setInternal] = React.useState(defaultChecked ?? false) and toggles it on change when no checked prop is supplied. State plus an event handler means the file must hydrate on the client, so importing the Checkbox into a Server Component automatically draws the boundary at that import — you do not add 'use client' to your page. This differs from the Button, which has no directive and stays server-rendered. The practical upshot: render the Checkbox wherever you like in the App Router, and Next.js ships exactly the small client bundle the component needs and nothing more.
1 // checkbox.tsx — the controlled/uncontrolled switch, verbatim 2 const [internal, setInternal] = React.useState(defaultChecked ?? false) 3 const isControlled = checked !== undefined 4 const isChecked = isControlled ? checked : internal
Submit a boolean field with a Server Action
The native <input type="checkbox"> inside the component is hidden with className="sr-only", which means it is invisible but still part of the form and still posts with the rest of the fields. Give the Checkbox a name and drop it inside a <form action={...}> bound to a Server Action — the box reads its value straight out of FormData with no onChange handler. A checked box sends its value (defaulting to the browser's "on"); an unchecked box is simply absent from the payload, which is the standard HTML convention you read with formData.get('name'). The whole form stays server-rendered except for the Checkbox itself, so the only client JavaScript is the few lines the component needs to toggle its own visual state.
1 // app/settings/page.tsx — server component 2 import { Checkbox } from '@/components/ui/checkbox' 3 import { Button } from '@/components/ui/button' 4 5 async function save(formData: FormData) { 6 'use server' 7 const subscribed = formData.get('newsletter') === 'on' 8 // persist the boolean 9 } 10 11 export default function Page() { 12 return ( 13 <form action={save}> 14 <Checkbox name="newsletter" label="Subscribe to the newsletter" /> 15 <Button type="submit">Save</Button> 16 </form> 17 ) 18 }
Control the checked state from React
When the UI has to react to the checkbox before submit — enabling a button, revealing a field, syncing two boxes — pass a checked prop and the component switches to controlled mode (isControlled = checked !== undefined). It then mirrors whatever you pass and fires onChange with the native change event, so you read e.target.checked to update your state. This requires a client component, so put the directive at the top of the small leaf file that owns the state, not on the whole page. The Checkbox examples page collects more of these patterns; the Input component pairs with it for fuller forms.
1 'use client' 2 import * as React from 'react' 3 import { Checkbox } from '@/components/ui/checkbox' 4 import { Button } from '@/components/ui/button' 5 6 export function TermsGate() { 7 const [agreed, setAgreed] = React.useState(false) 8 return ( 9 <div className="flex flex-col gap-3"> 10 <Checkbox 11 label="I accept the terms" 12 checked={agreed} 13 onChange={(e) => setAgreed(e.target.checked)} 14 /> 15 <Button type="submit" disabled={!agreed}>Continue</Button> 16 </div> 17 ) 18 }
Disabled state and accessibility
Pass disabled and the component adds opacity-50 cursor-default to the wrapping <label> and sets disabled on the hidden input, so clicks no longer toggle it. Because the styled box is a sibling <span> and the real control is the sr-only input wrapped in a <label>, clicking the label or its text checks the box and screen readers announce a real checkbox role and state — there is no role="checkbox" shim or manual ARIA to maintain. Keyboard focus and the spacebar toggle come for free from the native element. The tick is a Check icon rendered only when isChecked is true, sized w-2.5 h-2.5, sitting inside a w-4 h-4 box that flips to bg-primary border-primary when checked. Re-theme those by editing the color tokens the classes reference.
1 import { Checkbox } from '@/components/ui/checkbox' 2 3 <Checkbox label="Cannot change" disabled /> 4 <Checkbox label="Already accepted" checked disabled />
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
The component file carries the directive itself because it uses useState for its uncontrolled mode, so you never add 'use client' to the page that renders it — importing the Checkbox draws the client boundary at that import. Your surrounding page and form stay server components, and Next.js ships only the small client bundle the Checkbox needs to toggle its own visual state.
Yes. The real <input type="checkbox"> is hidden with sr-only rather than removed, so it stays in the form. Give the Checkbox a name and place it inside a <form action={...}> — a checked box posts its value through FormData and an unchecked box is absent, the standard HTML behavior. Read it on the server with formData.get('name'), no event handler required.
Pass a checked prop and the component enters controlled mode, mirroring your value and calling onChange with the native change event. Read e.target.checked inside the handler to update your state. Controlled usage needs a client component, so keep the directive on the small leaf file that owns the state rather than marking the whole page client.
Yes, because it wraps a genuine <input type="checkbox"> in a <label> rather than faking the control with a <div role="checkbox">. Clicking the label or its text toggles the box, the spacebar works, focus comes from the native element, and screen readers announce a real checkbox role and checked state. The visible box is a decorative sibling <span>, so no manual ARIA wiring is needed.

