Drivn vs shadcn/ui — Toggle Compared
Drivn builds its Toggle on plain React; shadcn/ui builds on Radix. Compare the zero-dependency build, the Toggle.Group API, and the variants side by side.
The fact that frames this comparison: shadcn/ui builds its Toggle on Radix and Drivn builds its Toggle on nothing but React. shadcn ships a Toggle that wraps @radix-ui/react-toggle and styles it with class-variance-authority (cva), and a separate ToggleGroup that wraps @radix-ui/react-toggle-group. Drivn ships a single Toggle file with zero runtime UI dependencies — pure React state, a data-[state=on] attribute, and Tailwind classes in a styles object. That difference in foundation is what every other difference on this page comes back to.
Drivn's Toggle lives at packages/drivn/src/registry/components/toggle.ts. The standalone button manages its own pressed state with React.useState, reflects it through aria-pressed and data-state, and exposes the same default and outline variants you would reach for in shadcn. The grouped form is Toggle.Group, attached with Object.assign so you get dot notation — Toggle.Group and Toggle from one import — backed by a small React context instead of a second Radix primitive. This page compares the two builds with verbatim source from the Drivn registry, and is honest about what you trade when you drop Radix: you give up Radix's roving-tabindex keyboard model, and you gain a single dependency-free file you fully own after install.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying primitive | None — plain React + Tailwind | @radix-ui/react-toggle + react-toggle-group |
| Install command | npx drivn add toggle | npx shadcn add toggle toggle-group |
| Runtime dependencies | Zero UI deps | Radix + class-variance-authority |
| Single + group in one file | Yes — Toggle and Toggle.Group | No — two separate components |
| Group API | Toggle.Group via React context | ToggleGroup via Radix context |
| Variants | default, outline | default, outline |
| Sizes | sm, md, lg | sm, default, lg |
| Pressed state attribute | data-[state=on] + aria-pressed | data-[state=on] + aria-pressed |
| Variant styling | styles object, no cva | cva with toggleVariants |
| License | MIT | MIT |
Radix primitive vs plain React
The starting point is the foundation. shadcn/ui's Toggle is a styled wrapper over @radix-ui/react-toggle, and its grouped form wraps a second package, @radix-ui/react-toggle-group. Drivn's Toggle imports nothing but React and the cn class helper. The pressed button is a React.forwardRef over a native <button> that tracks its own state with useState, sets aria-pressed, and flips a data-state attribute between on and off so Tailwind can style each state.
What you trade is real and worth naming: Radix gives the grouped toggle a roving-tabindex keyboard model and a battle-tested accessibility tree. Drivn's group is a lighter React context that toggles values but does not implement arrow-key roving focus. What you gain is a single file with zero UI dependencies that you own outright after install — no Radix version to track, no cva in your bundle. The standalone button still carries aria-pressed, so on its own it announces correctly to assistive tech.
1 // packages/drivn/src/registry/components/toggle.ts — verbatim 2 <button 3 ref={ref} 4 type="button" 5 aria-pressed={isPressed} 6 data-state={isPressed ? 'on' : 'off'} 7 disabled={disabled ?? (inGroup ? ctx.disabled : false)} 8 className={cn(styles.base, styles.variants[variant], styles.sizes[size], className)} 9 onClick={() => { 10 if (inGroup) { 11 ctx.onToggle(value) 12 } else { 13 const next = !isPressed 14 if (pressed === undefined) setInternal(next) 15 onChange?.(next) 16 } 17 }} 18 {...props} 19 >
One file ships both Toggle and Toggle.Group
shadcn/ui splits the single toggle and the grouped toggle into two components you add separately — toggle and toggle-group. Drivn ships both from one file using the Object.assign dot-notation pattern: the standalone ToggleButton is the default export, and Toggle.Group is attached to it. You write import { Toggle } from "@/components/ui/toggle" once and get both the button and the group, fully typed, with no second install.
The group is a plain React context. Toggle.Group holds the selected value — a string for type="single", a string array for type="multiple" — and each child Toggle with a value prop reads its pressed state from that context instead of its own useState. The same Toggle component works standalone or inside a group; it detects the context at render time. This is the dot-notation convention Drivn uses across compound components like Tabs and Dialog, so the API feels consistent once you have learned it once.
1 // packages/drivn/src/registry/components/toggle.ts — verbatim 2 export const Toggle = Object.assign(ToggleButton, { 3 Group: ToggleGroupRoot, 4 })
Variants without cva
Both libraries expose the same two visual variants — default and outline — but they get there differently. shadcn generates them with class-variance-authority, exporting a toggleVariants function that other components import. Drivn keeps every class in a co-located styles object and indexes into it with a typed key: styles.variants[variant] and styles.sizes[size], where variant is keyof typeof styles.variants. There is no cva in the bundle and no magic strings — the variant names autocomplete from the object itself.
The pressed look is driven entirely by the data-[state=on] attribute. Both the default and outline variants set data-[state=on]:bg-muted and data-[state=on]:text-foreground, so the active state reads as a filled muted surface in either case, with outline adding a border that adjusts on press. Because the classes live in your installed file, restyling a variant is a direct edit — change the object, not a config layer. The block below is verbatim from the registry.
1 // packages/drivn/src/registry/components/toggle.ts — verbatim 2 variants: { 3 default: cn( 4 'bg-transparent text-muted-foreground', 5 'hover:bg-muted hover:text-foreground', 6 'data-[state=on]:bg-muted', 7 'data-[state=on]:text-foreground' 8 ), 9 outline: cn( 10 'border border-border bg-transparent', 11 'text-muted-foreground', 12 'hover:border-foreground/20', 13 'data-[state=on]:bg-muted', 14 'data-[state=on]:text-foreground', 15 'data-[state=on]:border-muted' 16 ), 17 },
Single, multiple, and the controlled API
The grouped API matches what you would expect from shadcn, with one less primitive underneath. Toggle.Group takes type="single" for a one-active-toggle group — like a text-alignment picker — or type="multiple" for an independent set, like bold, italic, and underline in a text formatting toolbar. It supports defaultValue for uncontrolled use and a value plus onValueChange pair for controlled use, and an orientation="vertical" prop that switches the layout to a column.
Migrating a shadcn ToggleGroup to Drivn is mostly mechanical: the prop names line up (type, value, onValueChange, defaultValue), and the standalone Toggle keeps pressed, defaultPressed, and onChange. The main API change is the import — one Toggle instead of separate Toggle and ToggleGroup — and the keyboard caveat noted above for grouped focus. For the full set of usage patterns, the Toggle examples page walks single, multiple, sizes, disabled, and vertical groups one at a time.
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.Group>
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
No. shadcn/ui builds its Toggle on @radix-ui/react-toggle and its grouped form on @radix-ui/react-toggle-group. Drivn's 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, with zero runtime UI dependencies.
The main trade is the grouped keyboard model. Radix's ToggleGroup implements a roving tabindex so arrow keys move focus between items; Drivn's Toggle.Group is a lighter React context that toggles values without arrow-key roving focus. The standalone Toggle still sets aria-pressed and announces correctly, and you gain a single dependency-free file you fully own after install.
No. Drivn ships both from one file using the Object.assign dot-notation pattern, so a single npx drivn add toggle gives you the standalone Toggle and Toggle.Group together. shadcn/ui splits them into two components — toggle and toggle-group — that you add separately. In Drivn you import Toggle once and access the group as Toggle.Group.
Drivn keeps every class in a co-located styles object and indexes into it with a typed key, styles.variants[variant], where variant is keyof typeof styles.variants. There is no cva function and no magic strings — the variant names autocomplete from the object. shadcn generates the same default and outline variants with a cva toggleVariants function instead.
Pick Drivn when you want a zero-dependency Toggle you own outright — one file, no Radix version to track, no cva in your bundle, single and grouped forms together with dot notation. Pick shadcn when you specifically need Radix's roving-tabindex keyboard model for grouped toggles. For most toolbars and single toggles, Drivn's lighter build covers the use case.

