Skip to content
Drivn
6 min read

How to Add a Toggle Button to React

Step-by-step guide to add a toggle button to any React app with the Drivn CLI — render a single toggle, group several with Toggle.Group, and style it.

A toggle is a button that stays pressed — a Bold button in an editor, a grid-versus-list view switch, a filter that is simply on or off. Reach for it when a checkbox would look wrong and a switch would imply a saved setting rather than an action the user takes and sees. Adding one to a React app usually means either hand-rolling a button around useState and remembering the aria-pressed attribute, or pulling in a component library and its dependencies. Drivn's Toggle gives you the component as source you own, with zero runtime UI dependencies — no Radix, no cva — installed by one CLI command.

Because a toggle tracks whether it is pressed, its source opens with a 'use client' directive, so it runs on the client — true in any React setup, whether you scaffolded with Vite, Create React App, or Next.js. The component ships both forms from one file through dot notation: a standalone Toggle, and a Toggle.Group that coordinates several toggles through React context. This guide adds the Toggle to any React app: install the CLI, render a single toggle, group several into a formatting toolbar, switch a group to single-select, and restyle the pressed state. For the App Router specifics see the Next.js Toggle guide; for the shadcn/ui comparison see Drivn vs shadcn/ui Toggle.

Prerequisites

Before installing the Toggle, confirm your React project has the two things Drivn assumes: Tailwind CSS v4 processing your styles and a @/ path alias pointing at your source directory — the component ships as a .tsx file, so TypeScript is part of the stack. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, both are already wired; the installation page lists the minimal config for a custom setup. The Toggle has no runtime UI dependency of its own — no Radix, no cva — so nothing new gets version-locked into your bundle. Its only peer import is a lucide-react icon for the toggle's label, which most projects already have. And because the toggle holds interactive state through React.useState, its source carries a 'use client' directive — render it inside a client boundary, not directly in a framework's server-rendered tree.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Toggle source. The command writes a single toggle.tsx under your UI directory; there is no runtime Drivn package to version-lock, and the CLI docs cover the flags for targeting a custom path or adding several components in one run. After install the file is yours — a future Drivn release will not overwrite your edits — and the whole component, from the styles object through the Toggle.Group context, is a short file you can read top to bottom. If you also want an icon for the label, lucide-react is the only peer import. Commit the file for a clean baseline before wiring it in.

1# add the Toggle component source
2npx drivn add toggle

Step 2 — Render a single toggle

Start with one standalone toggle. Import Toggle and render it with an icon and a label as children — the base styles apply a gap-2 flex layout so they sit side by side. Left uncontrolled, the toggle owns its state: pass defaultPressed to set the starting value and it flips on each click, updating aria-pressed for assistive tech and a data-state attribute of on or off for the styling. To drive it from your own state instead, pass a pressed prop and an onChange callback — the controlled pattern you want when the pressed value lives in a store or a parent. This is the shape for a single boolean control that should read as a button rather than a checkbox: a Bold button, a Mute toggle, a Pin control. See the Toggle examples for the controlled and uncontrolled variants side by side.

1import { Toggle } from "@/components/ui/toggle"
2import { Bold } from "lucide-react"
3
4export default function Page() {
5 return (
6 <Toggle>
7 <Bold className="h-4 w-4" />
8 Bold
9 </Toggle>
10 )
11}

Step 3 — Group toggles into a toolbar

To let several toggles be active at once — the bold, italic, underline toolbar — wrap them in Toggle.Group with type="multiple". Each child takes a value, and the group tracks the active set as a string array through React context, so there is no shared useState to thread and no prop-drilling. Give the group a defaultValue array for an uncontrolled toolbar, or pass value with onValueChange to control it. Each toggle reads whether its value is in the active array and flips its data-state to match, so several can show the pressed styling at the same time. The same Toggle component works standalone or in a group — it detects the context at render time — so you reuse it unchanged. Pair the group with a Button for one-shot actions like "Clear formatting" that fire rather than toggle.

1<Toggle.Group type="multiple" defaultValue={["bold", "italic"]}>
2 <Toggle value="bold">
3 <Bold className="h-4 w-4" />
4 </Toggle>
5 <Toggle value="italic">
6 <Italic className="h-4 w-4" />
7 </Toggle>
8 <Toggle value="underline">
9 <Underline className="h-4 w-4" />
10 </Toggle>
11 <Toggle value="strikethrough">
12 <Strikethrough className="h-4 w-4" />
13 </Toggle>
14</Toggle.Group>

Step 4 — Single-select and vertical groups

When only one toggle can be active — text alignment, a view mode — set type="single" on the group. The value becomes a single string, selecting one clears the rest like a radio group, and 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 behaviour is otherwise identical, which suits a sidebar tool rail. To disable a whole group, set disabled on Toggle.Group — it passes down through context, and each child falls back to it unless it sets its own disabled. Switching a horizontal multiple-select toolbar to a vertical single-select list is a couple of attribute changes, not a rewrite, because every option is a plain prop on the group.

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>

Step 5 — Style the pressed state

Every visual difference between a pressed and an idle toggle comes from 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 — 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 object, and both props are typed against it so they autocomplete with no magic strings. To restyle the pressed state — a brand color instead of the muted fill — you edit the one data-[state=on]: line rather than threading a config prop, because Drivn writes the source into your repo instead of shipping a locked package. The block below is copied verbatim from toggle.ts. See theming to align the tokens it references with your app.

1const 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}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Install the Drivn CLI and run npx drivn add toggle, which writes toggle.tsx into your components directory. Import { Toggle } from "@/components/ui/toggle" and render it with an icon and label as children. Left uncontrolled it manages its own pressed state; pass pressed and onChange to control it. The component runs on the client because it tracks state with useState.

A toggle is a two-state button that stays pressed and announces its state through aria-pressed — use it for an action the user takes and sees, like a Bold button. A checkbox is a form input that represents a selected value submitted with a form. Reach for Toggle when the control reads as a button, and a checkbox when it belongs in a form's data.

Wrap the toggles in Toggle.Group with type="single". The group holds one active value as a string and clears the others when you select a new one, like a radio group; clicking the active toggle clears it back to empty. Use type="multiple" instead when several toggles should be independently on at once, which tracks the active set as an array.

No. The Toggle imports only React and a class-merge helper — it is a plain forwardRef button that tracks pressed state with useState and reflects it through aria-pressed and a data-state attribute. The grouped form uses a small React context rather than a second primitive, so npx drivn add toggle ships both the single and grouped toggle with zero runtime UI dependencies.

Yes. Set disabled on the Toggle.Group and it passes the disabled state down through context to every child; each toggle falls back to the group's value unless it sets its own disabled prop. This is the clean way to grey out an entire formatting bar while a document is read-only, without adding a disabled attribute to each toggle individually.