Next.js Toggle Component with Drivn
Add a toggle button to a Next.js App Router app: render it in a client component, build a formatting toolbar with Toggle.Group, and style the pressed state.
A toggle is a button that stays pressed — bold in a text editor, a grid-versus-list view switch, a filter that is either on or off. It is the two-state control you reach for when a checkbox would look wrong and a switch implies a persistent setting rather than an action. Drivn's Toggle gives you that button with no runtime UI dependency: it is a pure React button carrying aria-pressed and a data-state attribute — no Radix, no cva — and it ships as source you own after npx drivn add toggle. In a Next.js App Router app the one thing to get right is the client boundary, because a toggle holds interactive state and its source opens with a 'use client' directive for exactly that reason.
After install the component lives in src/components/ui/toggle.tsx. A single Toggle works controlled through pressed and onChange, or uncontrolled through defaultPressed, and Toggle.Group wires several toggles into a single- or multiple-select toolbar through React context — no prop-drilling. This guide installs Drivn in a Next.js 16 project, renders a toggle inside a client component, builds a formatting toolbar with Toggle.Group, switches to a single-select vertical group, and restyles the pressed state through the data-state hook. For the full reference see the Toggle docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Toggle.
Install in a Next.js 16 project
Drivn installs through a small CLI that writes the component source directly into your repository — there is no runtime UI package to version-lock, and no Radix primitive underneath. From the root of your Next.js 16 project run npx drivn add toggle. The command writes a single toggle.tsx under your UI directory; the CLI reference documents every flag, including targeting a custom path or adding several components at once. The only peer import is a lucide-react icon for the toggle's label, which most Drivn projects already have installed. After install the file is yours — a future Drivn release will not overwrite your edits — and the whole component, from the styles object to the Toggle.Group context, is a short file you can read top to bottom. Commit it for a clean baseline before wiring it in, then see the installation guide if you are adding Drivn to an existing project for the first time.
1 # from the root of your Next.js 16 project 2 npx drivn add toggle
Render a toggle in a client component
With the source installed, the smallest useful toggle is a single controlled button. Because a toggle tracks whether it is pressed, the component that renders it holds that state — mark the wrapper 'use client', keep the value in React.useState(false), and pass pressed and onChange. Drivn's Toggle sets aria-pressed from that value and mirrors it to a data-state of on or off, so screen readers announce the state and your styles can target it. Pass a lucide-react icon and a label as children; the base styles apply a gap-2 flex layout so the icon and text sit side by side. A Server Component page cannot own this state, which is why the toggle lives in a client wrapper you then import anywhere. Leave pressed off and pass defaultPressed instead for an uncontrolled toggle that manages its own state. See the Toggle examples for more variants.
1 'use client' 2 3 import * as React from 'react' 4 import { Toggle } from '@/components/ui/toggle' 5 import { Bold } from 'lucide-react' 6 7 export function BoldToggle() { 8 const [bold, setBold] = React.useState(false) 9 10 return ( 11 <Toggle pressed={bold} onChange={setBold}> 12 <Bold className="h-4 w-4" /> 13 Bold 14 </Toggle> 15 ) 16 }
Build a formatting toolbar with Toggle.Group
For a toolbar where several options can be active at once, wrap your toggles in Toggle.Group with type="multiple". Each child Toggle takes a value, and the group tracks the active set through React context — no prop-drilling and no shared useState in your component. Give the group a defaultValue array for an uncontrolled toolbar, or pass value and onValueChange to control it. Inside the group each toggle reads whether its value is in the active array and flips its data-state accordingly, so a pressed Bold and Italic both show the active styling at the same time. This is the classic rich-text toolbar pattern — bold, italic, underline, strikethrough — and because the group is context-driven you can drop new toggles in without rewiring state. The whole group is still a client component, so mount it inside a 'use client' wrapper. Pair it with a Button for actions that fire rather than toggle.
1 'use client' 2 3 import { Toggle } from '@/components/ui/toggle' 4 import { Bold, Italic, Underline, Strikethrough } from 'lucide-react' 5 6 export function Toolbar() { 7 return ( 8 <Toggle.Group type="multiple" defaultValue={["bold", "italic"]}> 9 <Toggle value="bold"> 10 <Bold className="h-4 w-4" /> 11 </Toggle> 12 <Toggle value="italic"> 13 <Italic className="h-4 w-4" /> 14 </Toggle> 15 <Toggle value="underline"> 16 <Underline className="h-4 w-4" /> 17 </Toggle> 18 <Toggle value="strikethrough"> 19 <Strikethrough className="h-4 w-4" /> 20 </Toggle> 21 </Toggle.Group> 22 ) 23 }
Single-select groups and vertical orientation
When only one option can be active — text alignment, a view mode — set type="single" on the group. Selecting a toggle now clears the others, exactly like a radio group, and the group's value is a single string rather than an array. The deselect behaviour is built in: in single mode, clicking the already-active toggle clears it back to an empty string, so the control can be turned off entirely. Add orientation="vertical" to stack the toggles in a column instead of a row — the group applies a flex-col class and the toggles lay out top to bottom, which suits a sidebar of view options. Both are plain attributes on Toggle.Group, so switching a horizontal multiple-select toolbar to a vertical single-select list is two attribute changes, not a rewrite. The alignment example shows the single-select pattern with left, center, right, and justify.
1 <Toggle.Group type="single" defaultValue="center"> 2 <Toggle value="left"> 3 <AlignLeft className="h-4 w-4" /> 4 </Toggle> 5 <Toggle value="center"> 6 <AlignCenter className="h-4 w-4" /> 7 </Toggle> 8 <Toggle value="right"> 9 <AlignRight className="h-4 w-4" /> 10 </Toggle> 11 <Toggle value="justify"> 12 <AlignJustify className="h-4 w-4" /> 13 </Toggle> 14 </Toggle.Group>
Style the pressed state with data-state
Every visual difference between a pressed and an idle toggle is driven by one attribute: data-state. Drivn sets it to on or off on the button, and the styles object targets it with Tailwind's data-[state=on]: variant — so a pressed toggle gets bg-muted and text-foreground while an idle one stays transparent with muted text. There are two variants, default and outline, and three sizes, sm, md, and lg, all held in the same styles object you own after install. To restyle the pressed state — say a brand color instead of the muted background — you edit the one data-[state=on]: line rather than threading a config prop. The block below is copied verbatim from toggle.ts; because you own the file, this is the whole surface you customize, with no variant map hidden behind a package boundary. See theming to align the tokens it references with the rest of your app.
1 const styles = { 2 base: cn( 3 'inline-flex items-center justify-center gap-2', 4 'rounded-md text-sm font-medium', 5 'transition-colors cursor-pointer', 6 'focus:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50', 7 'disabled:opacity-50 disabled:cursor-default' 8 ), 9 variants: { 10 default: cn( 11 'bg-transparent text-muted-foreground', 12 'hover:bg-muted hover:text-foreground', 13 'data-[state=on]:bg-muted', 14 'data-[state=on]:text-foreground' 15 ), 16 outline: cn( 17 'border border-border bg-transparent', 18 'text-muted-foreground', 19 'hover:border-foreground/20', 20 'data-[state=on]:bg-muted', 21 'data-[state=on]:text-foreground', 22 'data-[state=on]:border-muted' 23 ), 24 }, 25 sizes: { 26 sm: 'h-8 px-2.5 text-xs', 27 md: 'h-9 px-3 text-sm', 28 lg: 'h-10 px-4 text-sm', 29 }, 30 group: 'inline-flex items-center gap-1', 31 vertical: 'flex-col', 32 }
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
Because a toggle holds interactive state — it tracks whether it is pressed and, in a group, which values are active, using React.useState and context. Any component that renders those hooks must run on the client. The Toggle source opens with 'use client' for that reason, so you render it inside a client wrapper that a Server Component page can then import.
With type="multiple" the group tracks an array of active values, so several toggles can be pressed at once — the pattern for a formatting toolbar. With type="single" only one value is active at a time, like a radio group, and clicking the active toggle clears it back to an empty string. You choose between them with the type prop; everything else stays the same.
No. The Toggle is a plain React button styled with Tailwind and the local cn() helper — no Radix, no cva, and no runtime UI package. Group state runs on React context defined in the same file. Past React, its only import is a lucide-react icon for the label, and after install the whole component is source you own and can edit.
Edit the styles object in the installed toggle.tsx. The pressed appearance lives on the data-[state=on]: Tailwind variants inside each variant — swap bg-muted and text-foreground for your brand colors. Because Drivn writes the source into your repo instead of shipping a locked package, there is no config prop or theme override to thread through; you change the line directly.

