Tooltip
Krótka etykieta tekstowa, która pojawia się po najechaniu i przy fokusie z klawiatury, żeby nazwać kontrolkę z samą ikoną albo pokazać jej skrót.
import { PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export default function TooltipDemo() {
return (
<Tooltip>
<TooltipTrigger
render={
<Button variant="outline" size="icon" aria-label="Dodaj do biblioteki" />
}
>
<PlusIcon />
</TooltipTrigger>
<TooltipContent>Dodaj do biblioteki</TooltipContent>
</Tooltip>
)
}Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/tooltip.jsonOwiń aplikację jeden raz w TooltipProvider. Nadaje on wszystkim podpowiedziom to samo opóźnienie i sprawia, że gdy jedna jest już widoczna, sąsiednie otwierają się od razu — przesuń kursor wzdłuż paska narzędzi, a etykiety pojawią się bez czekania.
import { TooltipProvider } from "@/components/ui/tooltip"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<TooltipProvider delay={400}>{children}</TooltipProvider>
</body>
</html>
)
}Użycie
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"<Tooltip>
<TooltipTrigger render={<Button variant="outline" size="icon" aria-label="Add to library" />}>
<PlusIcon />
</TooltipTrigger>
<TooltipContent>Add to library</TooltipContent>
</Tooltip>Podpowiedzi prfct mają odwrócone kolory — tło w kolorze foreground, tekst w kolorze background — więc w obu trybach czyta się je jak etykietę nałożoną na dowolną powierzchnię. Pojawiają się też szybciej niż inne wyskakujące elementy (160 ms), bo w ciągu jednej sesji wyświetlają się dziesiątki razy.
Przykłady
Skróty klawiszowe
Podpowiedź to naturalne miejsce na naukę skrótów. Umieść Kbd po etykiecie — sam dopasuje wygląd do odwróconej powierzchni. Przesuń kursor wzdłuż paska narzędzi: gdy otworzy się pierwsza podpowiedź, kolejne pojawiają się natychmiast.
import {
BoldIcon,
CodeIcon,
ItalicIcon,
LinkIcon,
StrikethroughIcon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { Separator } from "@/components/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const actions = [
{ label: "Pogrubienie", icon: BoldIcon, keys: ["⌘", "B"] },
{ label: "Kursywa", icon: ItalicIcon, keys: ["⌘", "I"] },
{ label: "Przekreślenie", icon: StrikethroughIcon, keys: ["⌘", "⇧", "X"] },
{ label: "Kod w tekście", icon: CodeIcon, keys: ["⌘", "E"] },
{ label: "Link", icon: LinkIcon, keys: ["⌘", "K"] },
]
export default function TooltipShortcuts() {
return (
<div
role="toolbar"
aria-label="Formatowanie"
className="flex items-center gap-0.5 rounded-xl border bg-card p-1 shadow-raised"
>
{actions.map(({ label, icon: Icon, keys }, index) => (
<div key={label} className="contents">
{index === 3 && (
<Separator
orientation="vertical"
className="mx-1 h-5! self-center!"
/>
)}
<Tooltip>
<TooltipTrigger
render={
<Button variant="ghost" size="icon-sm" aria-label={label} />
}
>
<Icon />
</TooltipTrigger>
<TooltipContent>
{label}
<KbdGroup>
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
</TooltipContent>
</Tooltip>
</div>
))}
</div>
)
}Położenie
side umieszcza podpowiedź nad wyzwalaczem (top, domyślnie), po jego prawej stronie (right), pod nim (bottom) lub po lewej (left). Przy krawędziach widocznego obszaru podpowiedź sama przenosi się na przeciwną stronę.
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const sides = ["top", "right", "bottom", "left"] as const
export default function TooltipSides() {
return (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{sides.map((side) => (
<Tooltip key={side}>
<TooltipTrigger
render={<Button variant="outline" className="capitalize" />}
>
{side}
</TooltipTrigger>
<TooltipContent side={side}>Wyświetlana po stronie {side}</TooltipContent>
</Tooltip>
))}
</div>
)
}Wyłączone wyzwalacze
Wyłączony przycisk nie odbiera zdarzeń wskaźnika ani fokusu, więc podpowiedź na nim nigdy by się nie otworzyła. Zachowaj do niego dostęp przez focusableWhenDisabled i wyjaśnij w podpowiedzi, czego brakuje.
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export default function TooltipDisabled() {
return (
<Tooltip>
{/*
A disabled <button> receives no pointer or focus events, so its tooltip
could never open. `focusableWhenDisabled` keeps it reachable while
still blocking activation.
*/}
<TooltipTrigger render={<Button disabled focusableWhenDisabled />}>
Opublikuj
</TooltipTrigger>
<TooltipContent>Przed publikacją dodaj tytuł</TooltipContent>
</Tooltip>
)
}Opóźnienie
Provider ustawia opóźnienie 400 ms dla wszystkich podpowiedzi. Nadpisz je dla konkretnego wyzwalacza przez delay — krótsze w gęstych narzędziach, które przegląda się szybko, dłuższe tam, gdzie przypadkowe podpowiedzi by przeszkadzały.
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const delays = [
{ label: "Natychmiast", delay: 0 },
{ label: "Domyślnie", delay: undefined },
{ label: "Z opóźnieniem", delay: 1000 },
]
export default function TooltipDelay() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
{delays.map(({ label, delay }) => (
<Tooltip key={label}>
<TooltipTrigger delay={delay} render={<Button variant="outline" />}>
{label}
</TooltipTrigger>
<TooltipContent>
{delay === undefined
? "Dziedziczy 400 ms z providera"
: `Otwiera się po ${delay} ms`}
</TooltipContent>
</Tooltip>
))}
</div>
)
}Wytyczne
Kiedy używać
- Do nazywania przycisków z samą ikoną. Każdy przycisk z ikoną potrzebuje
aria-label; podpowiedź pokazuje ten sam tekst widzącym osobom, które korzystają z kursora. - Do pokazywania skrótów klawiszowych częstych akcji.
- Do rozwinięcia uciętego tekstu, na przykład długiej nazwy pliku.
Kiedy nie używać
- Do informacji potrzebnych do wykonania zadania — pokaż je na stronie. Podpowiedzi są niewidoczne, dopóki ktoś nie wpadnie na to, żeby ich poszukać.
- Do treści interaktywnych: linków, przycisków, pól. Podpowiedź znika, gdy kursor się oddali — użyj Popover.
- Do rozbudowanych podglądów — użyj Hover Card.
- W interfejsach wyłącznie dotykowych, gdzie nie ma najechania — zamiast tego pokaż etykietę na stałe.
Pisz etykiety, nie zdania
Podpowiedź czyta się w mgnieniu oka: dwa do czterech słów, wielka litera tylko na początku, bez kropki na końcu. Opisz akcję, nie ikonę.
Dostępność
Podpowiedź to wizualna wskazówka dla widzących osób korzystających z kursora i klawiatury. Otwiera się po najechaniu i przy fokusie z klawiatury, a sama nigdy nie przejmuje fokusu — ale Base UI nie wiąże jej z wyzwalaczem przez aria-describedby, więc czytniki ekranu jej nie odczytują. Znaczenie musi nieść dostępna nazwa samego wyzwalacza.
| Klawisz | Działanie |
|---|---|
Tab | Ustawienie fokusu na wyzwalaczu otwiera podpowiedź po upływie opóźnienia. |
Esc | Zamyka podpowiedź bez przenoszenia fokusu. |
- Umieść etykietę w dostępnej nazwie. Nadaj wyzwalaczom z samą ikoną
aria-labelo tej samej treści co podpowiedź. Podpowiedź uzupełnia nazwę — nigdy jej nie zastępuje. - Można na nią najechać. Przesunięcie kursora na podpowiedź nie zamyka jej, więc osoby korzystające z lupy ekranowej mogą ją przeczytać (WCAG 1.4.13).
disableHoverablePopupustawiaj tylko dla podpowiedzi, które podążają za kursorem. - Można ją zamknąć. Esc ukrywa ją bez przesuwania kursora ani fokusu.
- Tylko zwykły tekst. Treść podpowiedzi jest nieosiągalna z klawiatury; nigdy nie umieszczaj w niej linków ani przycisków.
Dokumentacja API
TooltipProvider
Współdzieli opóźnienie między podpowiedziami. Nie renderuje żadnego elementu.
Tooltip
Komponent główny. Nie renderuje żadnego elementu. Przyjmuje wszystkie propsy Tooltip.Root z Base UI.
TooltipTrigger
Element, który pokazuje podpowiedź. Renderuje <button>; użyj render, żeby wyrenderować Button z prfct.
TooltipContent
Renderuje portal, element pozycjonujący i odwrócony dymek ze strzałką.