Przejdź do treści

Hover Card

Podgląd treści, która kryje się za linkiem — osoby, repozytorium, strony — wyświetlany po najechaniu na link lub ustawieniu na nim fokusu.

Kod źródłowyBase UIRejestr

Zrecenzowane przez @grace 2 godz. temu

import { CalendarDaysIcon, MapPinIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

export default function HoverCardDemo() {
  return (
    <p className="text-sm text-muted-foreground">
      Zrecenzowane przez{" "}
      <HoverCard>
        <HoverCardTrigger
          href="#"
          className="font-medium text-foreground underline decoration-current/30 underline-offset-4 hover:decoration-current"
        >
          @grace
        </HoverCardTrigger>
        <HoverCardContent className="w-80">
          <div className="flex gap-4">
            <Avatar size="lg">
              <AvatarFallback>GH</AvatarFallback>
            </Avatar>
            <div className="flex min-w-0 flex-1 flex-col gap-2">
              <div className="flex items-start justify-between gap-2">
                <div className="flex flex-col">
                  <span className="text-sm font-semibold text-foreground">
                    Grace Hopper
                  </span>
                  <span className="text-xs text-muted-foreground">@grace</span>
                </div>
                <Button size="xs" variant="outline">
                  Obserwuj
                </Button>
              </div>
              <p className="text-sm leading-5 text-foreground">
                Inżynierka kompilatorów. Design systemy, narzędzia dla programistów i bardzo małe nanosekundy.
              </p>
              <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
                <span className="flex items-center gap-1">
                  <MapPinIcon className="size-3.5" />
                  Arlington, Wirginia
                </span>
                <span className="flex items-center gap-1">
                  <CalendarDaysIcon className="size-3.5" />
                  Dołączyła w marcu 2019
                </span>
              </div>
            </div>
          </div>
        </HoverCardContent>
      </HoverCard>{" "}
      2 godz. temu
    </p>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/hover-card.json

Użycie

import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
<HoverCard>
  <HoverCardTrigger href="/people/grace">@grace</HoverCardTrigger>
  <HoverCardContent>{/* profile preview */}</HoverCardContent>
</HoverCard>

HoverCardTrigger renderuje prawdziwy element <a>: podgląd tylko wzbogaca link, który działa samodzielnie. Komponent jest zbudowany na Preview Card z Base UI.

Przykłady

Podgląd zgłoszenia

Karta podglądu świetnie sprawdza się przy odwołaniach do zgłoszeń, pull requestów i dokumentów: odpowiada na pytanie co to jest i czy jest już zrobione? bez jednego kliknięcia. Ogranicz ją do nazwy, statusu i jednej lub dwóch linijek kontekstu.

Naprawione w DAT-1284, dostępne od wersji 1.4.2.

import { CircleDotIcon, MessageSquareIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

export default function HoverCardIssuePreview() {
  return (
    <p className="max-w-md text-center text-sm leading-6 text-muted-foreground">
      Naprawione w{" "}
      <HoverCard>
        <HoverCardTrigger
          href="#"
          className="font-mono font-medium text-link underline decoration-current/30 underline-offset-4 hover:decoration-current"
        >
          DAT-1284
        </HoverCardTrigger>
        <HoverCardContent className="flex w-80 flex-col gap-3">
          <div className="flex items-center justify-between gap-2">
            <span className="flex items-center gap-1.5 font-mono text-xs text-muted-foreground">
              <CircleDotIcon className="size-3.5 text-success-11" />
              DAT-1284
            </span>
            <Badge variant="success">
              <BadgeDot />
              Gotowe
            </Badge>
          </div>
          <p className="text-sm leading-5 font-medium text-foreground">
            Selektor daty blokuje fokus po otwarciu z popovera
          </p>
          <div className="flex items-center justify-between text-xs text-muted-foreground">
            <span className="flex items-center gap-2">
              <Avatar size="xs">
                <AvatarFallback>LT</AvatarFallback>
              </Avatar>
              Linus Torvalds · zamknął 2 dni temu
            </span>
            <span className="flex items-center gap-1 tabular-nums">
              <MessageSquareIcon className="size-3.5" />
              14
            </span>
          </div>
        </HoverCardContent>
      </HoverCard>
      , dostępne od wersji 1.4.2.
    </p>
  )
}

Pokaż, dokąd prowadzi link zewnętrzny, zanim ktoś zdecyduje się opuścić stronę. Tutaj karta otwiera się nad linkiem (side="top"), a metadane trafiają do zabarwionej stopki.

prfct jest zbudowany na mui/base-ui, prymitywach bez stylów od twórców Radix, Floating UI i Material UI.

import { GitForkIcon, StarIcon } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

export default function HoverCardLinkPreview() {
  return (
    <p className="max-w-md text-center text-sm leading-6 text-muted-foreground">
      prfct jest zbudowany na{" "}
      <HoverCard>
        <HoverCardTrigger
          href="https://github.com/mui/base-ui"
          target="_blank"
          rel="noreferrer"
          className="font-medium text-link underline decoration-current/30 underline-offset-4 hover:decoration-current"
        >
          mui/base-ui
        </HoverCardTrigger>
        <HoverCardContent side="top" className="w-80 p-0">
          <div className="flex flex-col gap-2 p-4">
            <div className="flex items-center gap-2">
              <span className="text-sm font-semibold text-foreground">
                mui/base-ui
              </span>
              <Badge variant="outline" size="sm">
                Publiczne
              </Badge>
            </div>
            <p className="text-sm leading-5 text-muted-foreground">
              Komponenty UI bez stylów do budowania dostępnych aplikacji webowych i design systemów.
            </p>
          </div>
          <div className="flex items-center gap-4 border-t bg-surface px-4 py-2.5 text-xs text-muted-foreground">
            <span className="flex items-center gap-1.5">
              <span className="size-2.5 rounded-full bg-blue-9" />
              TypeScript
            </span>
            <span className="flex items-center gap-1 tabular-nums">
              <StarIcon className="size-3.5" />
              7,9 tys.
            </span>
            <span className="flex items-center gap-1 tabular-nums">
              <GitForkIcon className="size-3.5" />
              410
            </span>
          </div>
        </HoverCardContent>
      </HoverCard>
      , prymitywach bez stylów od twórców Radix, Floating UI i Material UI.
    </p>
  )
}

Opóźnienie

delay (domyślnie 600 ms) i closeDelay (300 ms) na wyzwalaczu decydują, jak celowe musi być najechanie. Długie opóźnienia sprawiają, że karty nie migają, gdy kursor przemierza stronę; krótkie pasują do gęstych list, które ktoś świadomie przegląda.

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

const people = [
  {
    name: "Ada Lovelace",
    initials: "AL",
    role: "Maszyny analityczne",
    delay: 150,
  },
  { name: "Alan Turing", initials: "AT", role: "Obliczalność", delay: 600 },
  {
    name: "Katherine Johnson",
    initials: "KJ",
    role: "Mechanika orbitalna",
    delay: 1000,
  },
]

export default function HoverCardDelay() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-4">
      {people.map((person) => (
        <HoverCard key={person.name}>
          <HoverCardTrigger
            href="#"
            delay={person.delay}
            closeDelay={150}
            className="flex flex-col items-center gap-2 rounded-lg p-2 text-sm text-foreground focus-ring hover:bg-accent"
          >
            <Avatar size="lg">
              <AvatarFallback>{person.initials}</AvatarFallback>
            </Avatar>
            <span className="font-mono text-xs text-muted-foreground">
              {person.delay}ms
            </span>
          </HoverCardTrigger>
          <HoverCardContent className="w-60">
            <p className="text-sm font-semibold text-foreground">
              {person.name}
            </p>
            <p className="text-sm text-muted-foreground">{person.role}</p>
          </HoverCardContent>
        </HoverCard>
      ))}
    </div>
  )
}

Wytyczne

Kiedy używać

  • Do podglądu miejsca, do którego prowadzi link: osoby, zgłoszenia, repozytorium, dokumentu.
  • Gdy podgląd pomaga zdecydować, czy przejść dalej, ale do tego nie zmusza.

Kiedy nie używać

  • Do kluczowych informacji — na urządzeniach dotykowych karty podglądu w ogóle się nie pojawiają, a i bez tego łatwo je przeoczyć. Umieść takie informacje na stronie.
  • Do zadań interaktywnych — użyj komponentu Popover, który otwiera się po kliknięciu i może zawierać formularze.
  • Do jednolinijkowej etykiety — wystarczy Tooltip.

Podgląd, nie strona

Karta ma potwierdzić: tak, tego właśnie szukasz. Jeśli zaczynasz dodawać do niej zakładki, listy albo kilka akcji, podlinkuj zamiast tego osobną stronę.

GH
Grace HopperInżynierka kompilatorów · Arlington
DobrzeKto to jest i jedna linijka kontekstu — karta nie wchodzi w drogę.
Grace Hopper
ŹleCały pulpit w karcie podglądu znika, gdy tylko kursor się omsknie.

Dostępność

Karta podglądu to wizualne udogodnienie dla widzących osób, które korzystają z myszy lub klawiatury. Otwiera się po najechaniu na link i wtedy, gdy link dostaje fokus z klawiatury, ale nie jest ogłaszana, a link się do niej nie odwołuje — osoby korzystające z czytników ekranu poznają więc sam link. Traktuj kartę jako opcjonalny kontekst.

KlawiszDziałanie
Tab
Gdy link dostaje fokus, karta otwiera się po upływie opóźnienia.
Enter
Otwiera link.
Esc
Zamyka kartę.
  • Link musi się bronić sam. Jego tekst powinien mówić, dokąd prowadzi; karta tylko dodaje kontekst.
  • Nie ukrywaj w niej rzeczy kluczowych. Nic, co trzeba wiedzieć lub zrobić, nie może istnieć wyłącznie w karcie podglądu.
  • Można na nią najechać. Przesunięcie kursora z linku na kartę utrzymuje ją otwartą, o ile zmieścisz się w closeDelay.

Dokumentacja API

HoverCard

Komponent główny. Nie renderuje żadnego elementu. Przyjmuje wszystkie propsy PreviewCard.Root z Base UI.

PropTypDomyślnie
open

Czy karta jest otwarta. Używaj razem z onOpenChange, żeby nią sterować.

booleanBrak wartości domyślnej
defaultOpen

Czy karta jest otwarta na starcie, gdy nie jest kontrolowana.

booleanfalse
onOpenChange

Wywoływana przy otwarciu i zamknięciu.

(open: boolean, details) => voidBrak wartości domyślnej
actionsRef

Imperatywny uchwyt do zamknięcia lub odmontowania karty.

RefObject<{ close, unmount }>Brak wartości domyślnej

HoverCardTrigger

Link, który otwiera kartę. Renderuje <a> i przyjmuje wszystkie atrybuty odnośnika.

PropTypDomyślnie
hrefwymagany

Dokąd prowadzi link. Karta jest podglądem tego miejsca.

stringBrak wartości domyślnej
delay

Jak długo kursor musi spoczywać na linku, zanim karta się otworzy, w ms.

number600
closeDelay

Jak długo czekać z zamknięciem, gdy kursor opuści link, w ms.

number300

HoverCardContent

Renderuje portal, element pozycjonujący i wyskakujące okienko.

PropTypDomyślnie
side

Preferowana strona względem linku. Zmienia się na przeciwną, gdy karta się nie mieści.

"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end""bottom"
sideOffset

Odległość od linku, w px.

number6
align

Wyrównanie względem linku.

"start" | "center" | "end""center"
alignOffset

Przesunięcie wzdłuż osi wyrównania, w px.

number0
className

Łączona z klasami okienka. Domyślna szerokość to w-72.

stringBrak wartości domyślnej