Zum Inhalt springen
UI Kit
Accent

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

2 Variants

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.

Variants

Seiten und Kürzel

Vier Seiten, der Pfeil zeigt immer auf den Auslöser, auch wenn der Tipp ausweichen muss.

In einer Leiste

Icon-Knöpfe bekommen ihren Namen beim Hovern. Der ruhige Ton passt zu Hilfe neben Formularfeldern.

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

Props

Die Schnittstelle TooltipProps aus components/ui/tooltip.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • contentPflicht
    React.ReactNode

    The text. Keep it short, a tooltip is a label, not a manual.

  • keysoptional
    string[]

    Keyboard shortcut shown next to the text, e.g. ["⌘", "S"]

  • sideoptional
    Side
  • alignoptional
    Align
  • delayoptional
    number

    Delay before showing on hover, in ms. Focus shows it right away.

  • openoptional
    boolean

    Force it open, e.g. for a demo or an onboarding hint

  • toneoptional
    'default' | 'quiet'

    quiet: popover colors. Default is the inverted, high-contrast chip.

  • childrenPflicht
    React.ReactElement

    One 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
components/ui/tooltip.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert