Tooltip
A name for an icon-only control, in an inverted chip that fades in beside it.
The idea
Shown on hover and on focus, so the keyboard gets the same answer as the pointer, and it fades rather than travels: it appears beside something you are already looking at, and movement there only competes with what it names. For a row of icons that share one tooltip sliding between them, use Dock, which owns that as one piece of state.
Composes Dock.
Install
npm i mihirmodi-ui motionThe tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.
Usage
import { Tooltip } from "mihirmodi-ui";
<Tooltip label="GitHub" side="bottom">
<a href={repo} aria-label="GitHub"><GitHubMark /></a>
</Tooltip>Props
| Prop | Type | Notes |
|---|---|---|
| labelReactNode | ReactNode | The name. Keep the control's own aria-label too — a tooltip is not an accessible name. |
| side"top" | "right" | "bottom" | "left" | "top" | "right" | "bottom" | "left"= "top" | Where it appears. |
Source
Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.
"use client";
import { useState, type ReactNode } from "react";
export type TooltipProps = {
label: ReactNode;
children: ReactNode;
side?: "top" | "right" | "bottom" | "left";
className?: string;
};
/**
* A name for an icon-only control, in an inverted chip beside it.
*
* Shown on hover and on focus, so the keyboard gets the same answer as the
* pointer, and it is `role="tooltip"` with nothing interactive inside — a
* tooltip is a label, not a place to put a button. It fades rather than
* travels: it is appearing beside something you are already looking at, and
* movement there only competes with the thing it is naming.
*
* For a row of icons that share one tooltip sliding between them, use `Dock`,
* which owns that as one piece of state.
*/
export function Tooltip({ label, children, side = "top", className = "" }: TooltipProps) {
const [on, setOn] = useState(false);
const pos = {
right: "left-full top-1/2 -translate-y-1/2 ml-2",
left: "right-full top-1/2 -translate-y-1/2 mr-2",
top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
}[side];
return (
<span
className={`relative inline-flex ${className}`}
onMouseEnter={() => setOn(true)}
onMouseLeave={() => setOn(false)}
onFocus={() => setOn(true)}
onBlur={() => setOn(false)}
>
{children}
<span
role="tooltip"
className={`pointer-events-none absolute z-20 inline-flex h-[22px] items-center whitespace-nowrap rounded-md bg-ink px-2 text-[11px] font-medium leading-none text-background-100 shadow-[0_4px_10px_-4px_rgba(0,0,0,0.3)] transition-opacity duration-200 ease-out motion-reduce:transition-none ${pos} ${
on ? "opacity-100" : "opacity-0"
}`}
>
{label}
</span>
</span>
);
}