Drivn vs shadcn/ui — Tooltip Compared
shadcn/ui builds its Tooltip on Radix with a JS-driven open state; Drivn builds it on pure CSS group-hover. Compare the zero-dependency build side by side.
The fact that frames this comparison: shadcn/ui builds its Tooltip on Radix and Drivn builds its Tooltip on nothing but CSS. shadcn ships a Tooltip that wraps @radix-ui/react-tooltip — a TooltipProvider, a portal, a JS-driven open state with hover and focus delays, and collision-aware positioning. Drivn ships a single Tooltip file with zero runtime UI dependencies and, notably, zero JavaScript for the show/hide behaviour: the tip appears through Tailwind's group-hover and group-focus-within classes alone. That difference in foundation is what every other difference on this page comes back to.
Drivn's Tooltip lives at packages/drivn/src/registry/components/tooltip.ts. It is a <span> wrapper with the group class and tabIndex={0}, holding your trigger and a second <span> for the tip that starts at opacity-0 and transitions to opacity-100 on group-hover or group-focus-within. There is no useState, no provider to mount at the app root, and no portal. 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 the portal, configurable delays, and collision detection, and you gain a dependency-free, no-JavaScript tooltip you fully own after install.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying primitive | None — pure CSS group-hover | @radix-ui/react-tooltip |
| Install command | npx drivn add tooltip | npx shadcn add tooltip |
| Runtime dependencies | Zero UI deps | Radix UI |
| JavaScript for show/hide | None — CSS only | JS open state |
| Provider required | No | TooltipProvider |
| Renders in a portal | No — inline span | Yes |
| Positions | top, bottom, left, right | top, bottom, left, right + align |
| Open/close delay | CSS transition only | Configurable delayDuration |
| Collision detection | No | Yes — flips on overflow |
| Keyboard / focus support | group-focus-within | Radix focus management |
| License | MIT | MIT |
Radix primitive vs pure CSS
The starting point is the foundation. shadcn/ui's Tooltip is a styled wrapper over @radix-ui/react-tooltip: you mount a TooltipProvider, compose Tooltip, TooltipTrigger, and TooltipContent, and Radix tracks an open state in JavaScript, renders the content in a portal, and positions it with collision detection. Drivn's Tooltip imports nothing but React and the cn class helper, and it never tracks state at all. The wrapper is a <span> with the Tailwind group class; the tip is a sibling <span> that starts at opacity-0 and flips to opacity-100 whenever the group is hovered or holds focus.
What you trade is real and worth naming: Radix gives you a portal that escapes overflow: hidden parents, configurable open and close delays, and collision-aware flipping when the tip would clip the viewport. Drivn renders the tip inline, so a parent with overflow: hidden can clip it, and there is no delay or auto-flip. What you gain is a tooltip with zero UI dependencies and zero JavaScript for show and hide — no provider to mount, no portal, no Radix version to track — that you own outright after install.
1 // packages/drivn/src/registry/components/tooltip.ts — verbatim 2 return ( 3 <span 4 tabIndex={0} 5 className={cn(styles.base, 'outline-none', className)} 6 > 7 {children} 8 <span className={cn(styles.tip, styles.positions[position])}> 9 {content} 10 </span> 11 </span> 12 )
No provider, no portal, no state
shadcn/ui's Tooltip needs a TooltipProvider mounted somewhere above it — typically at the app root — because Radix shares delay timing and open state across tooltips through context. Drivn needs nothing of the sort. Each Tooltip is fully self-contained: a group wrapper and a tip span, with the visibility driven entirely by the group-hover:opacity-100 and group-focus-within:opacity-100 Tailwind classes. There is no React state, no effect, and no portal — what you render is what ends up in the DOM, in place.
The practical effect is that adding a tooltip is a one-line wrap with no setup tax. You do not register a provider, you do not manage an open prop, and there is no hydration boundary to think about — the component is not even marked 'use client' because it runs no client-side logic. The cost of that simplicity is the inline rendering noted above: because the tip is a normal positioned span rather than a portalled layer, it participates in its parent's stacking and clipping context. For most buttons, icons, and labels that is exactly what you want; for a tooltip inside a scroll container with hidden overflow, it is the trade to weigh.
1 // packages/drivn/src/registry/components/tooltip.ts — verbatim 2 tip: cn( 3 'absolute z-50 px-2.5 py-1.5 text-xs font-medium', 4 'rounded-lg bg-foreground text-background', 5 'whitespace-nowrap pointer-events-none', 6 'opacity-0 group-hover:opacity-100', 7 'group-focus-within:opacity-100 transition-opacity' 8 ),
Four positions from a typed object
Both libraries place the tip on any of four sides, but they get there differently. Radix takes a side prop and computes the final position at runtime, flipping it if it would collide with the viewport edge. Drivn takes a position prop typed as keyof typeof styles.positions and indexes straight into a styles.positions object — top, bottom, left, or right — each a static set of Tailwind utilities that absolutely-position and center the tip relative to the trigger. The position names autocomplete from the object itself, with no magic strings.
Because the placement is plain CSS, there is no collision detection: a top tooltip stays on top even near the top edge of the screen. That is the deliberate trade for zero JavaScript. In exchange, restyling or repositioning is a direct edit to the styles.positions object in your installed file — change mb-2 to mb-3, add an arrow, or introduce a new position key — rather than configuring a Radix prop layer. The block below is verbatim from the registry.
1 // packages/drivn/src/registry/components/tooltip.ts — verbatim 2 positions: { 3 top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', 4 bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', 5 left: 'right-full top-1/2 -translate-y-1/2 mr-2', 6 right: 'left-full top-1/2 -translate-y-1/2 ml-2', 7 },
Migrating from shadcn, and when to keep Radix
Moving a shadcn Tooltip to Drivn collapses the four-part composition into a single wrap. Where shadcn nests Tooltip, TooltipTrigger, and TooltipContent under a TooltipProvider, Drivn takes the trigger as children and the tip text as a content string prop — <Tooltip content="Copy">{trigger}</Tooltip>. You also delete the provider at your app root. The side prop maps to Drivn's position, and any delayDuration you configured has no equivalent because there is no JS delay to tune.
Keep Radix when you genuinely need what it provides: a portal so the tip escapes a clipping parent, collision detection so it flips near the viewport edge, or precise open and close delays for a dense UI. Reach for Drivn's Tooltip when you want a dependency-free, no-JavaScript hover hint that you own outright — which covers the large majority of icon buttons and labels. The Tooltip examples page walks the positions, multiline content, and icon triggers one at a time.
1 <Tooltip content="Copy to clipboard" position="top"> 2 <button className="rounded-md border border-border p-2"> 3 <Copy className="h-4 w-4" /> 4 </button> 5 </Tooltip>
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 Tooltip on @radix-ui/react-tooltip with a provider, a portal, and a JavaScript-driven open state. Drivn's Tooltip imports only React and a class-merge helper, and it shows and hides the tip with pure CSS — Tailwind group-hover and group-focus-within classes — so it ships with zero runtime UI dependencies and no JavaScript for the visibility behaviour.
Three things, all worth naming. You give up the portal, so a parent with overflow: hidden can clip the tip; you give up configurable open and close delays, since visibility is a CSS transition; and you give up collision detection, so a top tooltip stays on top even near the viewport edge. In exchange you gain a dependency-free, no-JavaScript tooltip you fully own after install.
No. shadcn/ui requires a TooltipProvider above your tooltips because Radix shares delay timing and open state through context. Drivn needs nothing of the sort — each Tooltip is self-contained, driven entirely by group-hover and group-focus-within classes, with no React state or effect. You wrap a trigger in a single Tooltip and it works with no app-root setup.
Drivn takes a position prop typed as keyof typeof styles.positions and indexes into a styles.positions object holding static Tailwind utilities for top, bottom, left, and right. Each set absolutely-positions and centers the tip relative to the trigger. The names autocomplete from the object with no magic strings. shadcn computes placement at runtime with Radix and flips it on collision instead.
Pick Drivn when you want a zero-dependency tooltip with no JavaScript, no provider, and no portal — one file you own outright, ideal for icon buttons and labels. Pick shadcn when you specifically need a portal to escape clipping parents, collision-aware flipping near the viewport edge, or tunable open and close delays. For most hover hints, Drivn's pure-CSS build covers the use case.

