Skip to content
Drivn
5 min read

How to Add a Tooltip to React

Step-by-step guide to add a tooltip to any React app with the Drivn CLI — show it on hover and keyboard focus, position it four ways, and style it.

A tooltip is the small hint that appears when a user hovers a control — the full label on an icon button, a keyboard shortcut on a menu item, an explanation beside a form field. Adding one to a React app is often heavier than the feature deserves: reach for a component library and you usually pull in a positioning engine like Floating UI or Popper, plus the state and portal wiring that comes with it. Drivn's Tooltip takes the opposite approach — it is a pure CSS component with zero runtime UI dependencies, shown and hidden entirely by Tailwind's group-hover and group-focus-within variants, with no JavaScript state at all.

That design has a practical payoff: because the Tooltip holds no state and calls no hooks, its source carries no 'use client' directive, so it renders inside a React Server Component without a client boundary — rare for a component that feels interactive. You give it a content string and a position, wrap the trigger as children, and Tailwind does the rest on hover and on keyboard focus. This guide adds the Tooltip to any React app: install the CLI, render a tooltip over a trigger, position it on any of the four sides, and restyle the bubble. For the Next.js specifics see the Next.js Tooltip guide; for the shadcn/ui comparison see Drivn vs shadcn/ui Tooltip.

Prerequisites

Before installing the Tooltip, 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 Tooltip has no runtime UI dependency — no Floating UI, no Popper, no Radix — so nothing new gets version-locked into your bundle, and it needs no provider at your app root. One layout detail matters: the bubble is positioned with plain CSS relative to its trigger, so the trigger must not be clipped by an ancestor with overflow: hidden on the side the tooltip opens toward.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Tooltip source. The command writes a single tooltip.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 is a short file you can read top to bottom, from the styles object through the two nested <span> elements. The Tooltip has no peer imports beyond React and the cn class-merge helper, so there is nothing else to add. Commit the file for a clean baseline before wiring it in.

1# add the Tooltip component source
2npx drivn add tooltip

Step 2 — Render a tooltip

Import Tooltip and wrap any trigger element as its children, passing the hint text through the content prop. The trigger renders inline, and the bubble is a sibling <span> that stays hidden until the user hovers or focuses the wrapper. There is no open state to manage and no onOpenChange handler — visibility is driven entirely by Tailwind's group-hover:opacity-100 and group-focus-within:opacity-100 variants on the bubble, so the tooltip shows on mouse hover and, because the wrapper carries tabIndex={0}, on keyboard focus too. That focus path is what makes it usable without a mouse. Wrap an icon button to label it, or wrap a snippet of text to explain a term. The content prop is a plain string, which keeps the markup flat and the accessible name predictable.

1import { Tooltip } from "@/components/ui/tooltip"
2
3export default function Page() {
4 return (
5 <Tooltip content="This is a tooltip">
6 <span>Hover me</span>
7 </Tooltip>
8 )
9}

Step 3 — Position it on any side

The position prop places the bubble on one of four sides — top (the default), bottom, left, or right — and each maps to a preset entry in the styles.positions object. The top value anchors the bubble with bottom-full left-1/2 -translate-x-1/2 mb-2, sitting it above the trigger and centering it horizontally; left and right center it vertically instead. Because the positions are plain Tailwind utility strings rather than a runtime layout calculation, there is no measurement pass and no flip-on-collision logic — you pick the side that has room. Choose top or bottom for inline text and toolbar buttons, and left or right in a vertical list or sidebar where a stacked tooltip would overlap the next row. The prop is typed as keyof typeof styles.positions, so the four values autocomplete with no magic strings.

1<Tooltip content="Top tooltip" position="top">
2 <span>Top</span>
3</Tooltip>
4
5<Tooltip content="Right tooltip" position="right">
6 <span>Right</span>
7</Tooltip>

Step 4 — Style the bubble

Every visual detail of the bubble lives in the tip entry of the styles object, and because Drivn writes the source into your repo you edit it directly rather than threading a config prop. By default the bubble is a small dark pill — rounded-lg bg-foreground text-background with px-2.5 py-1.5 text-xs font-medium — that fades in over a transition-opacity from opacity-0. It is marked pointer-events-none so it never intercepts a click meant for the trigger, and whitespace-nowrap so short hints stay on one line. To restyle it — a lighter surface, a wider bubble, a slower fade — you change these Tailwind classes in the one object. The block below is copied verbatim from tooltip.ts. See theming to align the bg-foreground and text-background tokens it references with your palette, and the Tooltip reference for the full props table.

1const styles = {
2 base: 'relative inline-flex group',
3 tip: cn(
4 'absolute z-50 px-2.5 py-1.5 text-xs font-medium',
5 'rounded-lg bg-foreground text-background',
6 'whitespace-nowrap pointer-events-none',
7 'opacity-0 group-hover:opacity-100',
8 'group-focus-within:opacity-100 transition-opacity'
9 ),
10 positions: {
11 top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
12 bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
13 left: 'right-full top-1/2 -translate-y-1/2 mr-2',
14 right: 'left-full top-1/2 -translate-y-1/2 ml-2',
15 },
16}
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 tooltip, which writes tooltip.tsx into your components directory. Import { Tooltip } from "@/components/ui/tooltip", wrap your trigger element as children, and pass the hint through the content prop. The tooltip shows on hover and keyboard focus with no state to manage, because visibility is pure CSS driven by Tailwind's group-hover variant.

No. The Tooltip is pure CSS with zero runtime UI dependencies — no Floating UI, no Popper, no Radix. The bubble is an absolutely positioned span placed with preset Tailwind utility classes for each of the four sides, so there is no measurement pass or JavaScript layout calculation. You choose the side with room via the position prop.

Yes. The wrapper element carries tabIndex={0} so it can receive keyboard focus, and the bubble reveals through Tailwind's group-focus-within:opacity-100 variant in addition to group-hover. That means a keyboard user who tabs to the trigger sees the tooltip the same way a mouse user does on hover, without any extra wiring on your part.

Yes. Because the Tooltip holds no state and calls no hooks, its source carries no 'use client' directive, so it renders directly inside a React Server Component without a client boundary. Visibility is handled entirely by CSS hover and focus variants rather than JavaScript, which is what lets a component that feels interactive stay fully server-rendered.

Pass the position prop with one of four values — top, bottom, left, or right — and the bubble anchors to that side of the trigger, with top as the default. Each value maps to a preset entry in the styles.positions object built from Tailwind utilities, and the prop is typed against that object so the four options autocomplete with no magic strings.