Skip to content
Drivn
5 min read

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

FeatureDrivnshadcn/ui
Underlying primitiveNone — plain React + Tailwind@radix-ui/react-toggle + react-toggle-group
Install commandnpx drivn add togglenpx shadcn add toggle toggle-group
Runtime dependenciesZero UI depsRadix + class-variance-authority
Single + group in one fileYes — Toggle and Toggle.GroupNo — two separate components
Group APIToggle.Group via React contextToggleGroup via Radix context
Variantsdefault, outlinedefault, outline
Sizessm, md, lgsm, default, lg
Pressed state attributedata-[state=on] + aria-presseddata-[state=on] + aria-pressed
Variant stylingstyles object, no cvacva with toggleVariants
LicenseMITMIT

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
2export 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
2variants: {
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>
Get started

Install Drivn in one command

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

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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.