Tooltip für React
Kurze Beschriftungen bei Hover und Fokus, optional mit Tastenkürzel. Escape blendet aus, wichtige Inhalte gehören nie hinein.
Eine von 9 Components in Overlays
Kurze Beschriftung, nie die einzige
Ein Tooltip nennt, was ein Icon tut, wenn für ein Wort kein Platz ist. Dieser öffnet sich bei Hover und bei Tastaturfokus, verschwindet mit Escape, verbindet sich über aria-describedby mit seinem Auslöser und bleibt dem Zeiger aus dem Weg.
„Seiten und Kürzel“ zeigt den Tooltip oben, unten, links und rechts, auf Wunsch mit Tastenkürzel wie Strg B in einer Tastenkappe. „In einer Leiste“ zeigt den typischen Einsatz, eine Reihe Icon-Buttons für einen Editor, jeder mit Name und Kürzel und einer kurzen Verzögerung, damit sich das Entlanggleiten ruhig anfühlt.
Halte Tooltips bei wenigen Wörtern. Links und Buttons gehören nicht hinein, dafür gibt es Popover und Hover Card. Touchscreens kennen kein Hover, der Icon-Button braucht also weiter sein eigenes aria-label, der Tooltip ergänzt nur den sichtbaren Hinweis.
Details
Was Tooltip mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Overlays
- Bereich
- Elements
- Variants
- 2
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/tooltip.tsx
- Bringt mit
- utilskitpopover
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle TooltipProps aus components/ui/tooltip.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
contentPflichtReact.ReactNodeThe text. Keep it short, a tooltip is a label, not a manual.
keysoptionalstring[]Keyboard shortcut shown next to the text, e.g. ["⌘", "S"]
sideoptionalSidealignoptionalAligndelayoptionalnumberDelay before showing on hover, in ms. Focus shows it right away.
openoptionalbooleanForce it open, e.g. for a demo or an onboarding hint
toneoptional'default' | 'quiet'quiet: popover colors. Default is the inverted, high-contrast chip.
childrenPflichtReact.ReactElementOne focusable element. It gets aria-describedby.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Tooltip reagiert selbst auf die Tasten Escape.
- Nutzt die Rolle tooltip.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 135 Zeilen
'use client';
import { cloneElement, isValidElement, useEffect, useId, useRef, useState } from 'react';
import { cn } from '@/lib/kit';
import { Floating, originFor, useMounted, usePresence, type Align, type FloatingPosition, type Side } from '@/components/ui/popover';
/* The tip slides a few pixels away from its trigger while it fades in, and back toward it when it leaves */
const FROM: Record<Side, string> = {
top: 'starting:translate-y-1 data-[closing]:translate-y-0.5',
bottom: 'starting:-translate-y-1 data-[closing]:-translate-y-0.5',
left: 'starting:translate-x-1 data-[closing]:translate-x-0.5',
right: 'starting:-translate-x-1 data-[closing]:-translate-x-0.5',
};
export interface TooltipProps {
/** The text. Keep it short, a tooltip is a label, not a manual. */
content: React.ReactNode;
/** Keyboard shortcut shown next to the text, e.g. ["⌘", "S"] */
keys?: string[];
side?: Side;
align?: Align;
/** Delay before showing on hover, in ms. Focus shows it right away. */
delay?: number;
/** Force it open, e.g. for a demo or an onboarding hint */
open?: boolean;
/** quiet: popover colors. Default is the inverted, high-contrast chip. */
tone?: 'default' | 'quiet';
className?: string;
/** One focusable element. It gets aria-describedby. */
children: React.ReactElement;
}
function Arrow({ pos, tone }: { pos: FloatingPosition | null; tone: 'default' | 'quiet' }) {
if (!pos) return null;
const s = pos.side;
const style: React.CSSProperties =
s === 'bottom' ? { top: -4, left: pos.arrow - 4 } : s === 'top' ? { bottom: -4, left: pos.arrow - 4 } : s === 'right' ? { left: -4, top: pos.arrow - 4 } : { right: -4, top: pos.arrow - 4 };
const borders = tone === 'quiet' ? (s === 'bottom' ? 'border-l border-t' : s === 'top' ? 'border-r border-b' : s === 'right' ? 'border-l border-b' : 'border-r border-t') : '';
return <span aria-hidden className={cn('absolute size-2 rotate-45 rounded-[1px]', tone === 'quiet' ? 'border-border bg-popover' : 'bg-foreground', borders)} style={style} />;
}
/**
* A short label on hover and focus. Escape hides it.
* <Tooltip content="Archive" keys={["E"]}><IconButton icon="archive" label="Archive" /></Tooltip>
*/
export function Tooltip({ content, keys, side = 'top', align = 'center', delay = 450, open: forced, tone = 'default', className, children }: TooltipProps) {
const [open, setOpen] = useState(false);
const mounted = useMounted();
const anchorRef = useRef<HTMLSpanElement>(null);
const timer = useRef<number | undefined>(undefined);
const id = useId();
const visible = forced ?? open;
const { present, closing } = usePresence(visible, 120);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setOpen(false);
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [open]);
useEffect(() => () => window.clearTimeout(timer.current), []);
const show = (now: boolean) => {
window.clearTimeout(timer.current);
if (now) setOpen(true);
else timer.current = window.setTimeout(() => setOpen(true), delay);
};
const hide = () => {
window.clearTimeout(timer.current);
setOpen(false);
};
const child = isValidElement<Record<string, unknown>>(children)
? cloneElement(children, { 'aria-describedby': cn(children.props['aria-describedby'] as string | undefined, id) || undefined })
: children;
return (
<>
<span
ref={anchorRef}
className="inline-flex"
onPointerEnter={(e) => e.pointerType === 'mouse' && show(false)}
onPointerLeave={hide}
onFocus={(e) => (e.target as HTMLElement).matches(':focus-visible') && show(true)}
onBlur={hide}
onPointerDown={hide}
>
{child}
</span>
{/* Server and closed state: the text stays in the DOM for aria-describedby */}
{!(mounted && present) && (
<span id={id} hidden>
{content}
</span>
)}
{mounted && present && (
<Floating anchor={anchorRef} side={side} align={align} offset={8} className="pointer-events-none">
{(pos) => (
<div
id={id}
role="tooltip"
data-closing={closing || undefined}
className={cn(
'relative flex max-w-[16rem] items-center gap-2 rounded-md px-2.5 py-1.5 text-[12.5px] font-medium leading-[18px] transition-[opacity,scale,translate] duration-200 ease-[cubic-bezier(0.34,1.4,0.64,1)] starting:scale-90 starting:opacity-0 data-[closing]:scale-95 data-[closing]:opacity-0 data-[closing]:duration-[120ms] data-[closing]:ease-[var(--ease-kit)] motion-reduce:transition-none',
'origin-[var(--origin)]',
FROM[pos?.side ?? side],
tone === 'quiet' ? 'border border-border bg-popover text-popover-foreground shadow-[var(--shadow-md)]' : 'bg-foreground text-background shadow-[var(--shadow-md)]',
className,
)}
style={{ '--origin': originFor(pos, side) } as React.CSSProperties}
>
<Arrow pos={pos} tone={tone} />
<span className="relative">{content}</span>
{keys && (
<span className="relative flex gap-0.5">
{keys.map((k) => (
<kbd
key={k}
className={cn(
'grid h-[18px] min-w-[18px] place-items-center rounded-[4px] px-1 font-sans text-[11px] font-medium',
tone === 'quiet' ? 'border border-border bg-surface text-muted-foreground' : 'bg-[color-mix(in_oklab,var(--background)_16%,transparent)] text-[color-mix(in_oklab,var(--background)_78%,var(--foreground))]',
)}
>
{k}
</kbd>
))}
</span>
)}
</div>
)}
</Floating>
)}
</>
);
}Häufige Fragen
Wie installiere ich Tooltip?
Mit aiw add @ai-website/tooltip. Der Befehl legt die Datei components/ui/tooltip.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind popover. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Tooltip?
2 Varianten, Seiten und Kürzel und In einer Leiste.
Kann ein KI-Assistent Tooltip einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Tooltip mit aiw_components und installieren sie mit aiw_add @ai-website/tooltip.