HextaUI

Hover card

Bir bağlantının üzerine gelindiğinde ya da odaklanıldığında açılan, görme engeli olmayan kullanıcıların göz atabileceği içerikler için bir önizleme kartı.

Shipped by @mira, reviewed by @jun and tested with a screen reader by @sol.

"use client"

import { IconMapPin } from "@tabler/icons-react"

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

type Person = {
  handle: string
  name: string
  initials: string
  bio: string
  location: string
}

const people: Record<string, Person> = {
  mira: {
    handle: "mira",
    name: "Mira Okafor",
    initials: "MO",
    bio: "Design engineer. Obsessed with easing curves.",
    location: "Lagos",
  },
  jun: {
    handle: "jun",
    name: "Jun Park",
    initials: "JP",
    bio: "Maintains the motion tokens and keeps the docs honest about what ships.",
    location: "Seoul",
  },
  sol: {
    handle: "sol",
    name: "Sol Ferreira",
    initials: "SF",
    bio: "Accessibility.",
    location: "Lisbon",
  },
}

const profile = createHoverCardHandle<Person>()

function Mention({ person }: { person: Person }) {
  return (
    <HoverCardTrigger
      handle={profile}
      payload={person}
      href="#"
      delay={250}
      render={
        <a className="font-medium text-foreground underline decoration-border underline-offset-4 hover:decoration-foreground" />
      }
    >
      @{person.handle}
    </HoverCardTrigger>
  )
}

export function HoverCardDemo() {
  return (
    <>
      <p className="max-w-sm text-center text-sm/relaxed text-muted-foreground">
        Shipped by <Mention person={people.mira} />, reviewed by{" "}
        <Mention person={people.jun} /> and tested with a screen reader by{" "}
        <Mention person={people.sol} />.
      </p>
      <HoverCard handle={profile}>
        {({ payload }) => (
          <HoverCardContent arrow>
            {payload && (
              <div className="flex gap-3">
                <Avatar>
                  <AvatarFallback>{payload.initials}</AvatarFallback>
                </Avatar>
                <div className="flex min-w-0 flex-col gap-1">
                  <p className="font-medium">{payload.name}</p>
                  <p className="text-muted-foreground">{payload.bio}</p>
                  <p className="flex items-center gap-1 pt-1 text-xs text-muted-foreground">
                    <IconMapPin className="size-3.5" aria-hidden="true" />
                    {payload.location}
                  </p>
                </div>
              </div>
            )}
          </HoverCardContent>
        )}
      </HoverCard>
    </>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/hover-card.json

Bileşeni, HextaUI tema token'larını ve bileşenin bağımlı olduğu tüm HextaUI bileşenlerini ekler.

import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
<HoverCard>
  <HoverCardTrigger href="/profile" render={<Button variant="link" nativeButton={false} render={<a />} />}>
    @hextaui
  </HoverCardTrigger>
  <HoverCardContent>
    Components built on shadcn/ui.
  </HoverCardContent>
</HoverCard>

Bir hover card bir önizlemedir; menü veya diyalog değildir. Trigger normal bir bağlantı olarak kalır; bu yüzden karttaki her şey bağlantının gittiği sayfada da bulunmalıdır.

HoverCard
├── HoverCardTrigger
└── HoverCardContent

Taraf

<HoverCardContent /> üzerinde side ve align ayarlayın. inline-end gibi mantıksal taraflar okuma yönünü izler ve kart ekrandan çıkacağında ters döner veya kayar.

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

const sides = ["top", "inline-end", "bottom", "inline-start"] as const

export function HoverCardSides() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      {sides.map((side) => (
        <HoverCard key={side}>
          <HoverCardTrigger
            href="#"
            delay={200}
            render={
              <Button variant="outline" nativeButton={false} render={<a />} />
            }
          >
            {side}
          </HoverCardTrigger>
          <HoverCardContent side={side} className="w-48">
            Opens on the {side} side, and flips when there isn’t room.
          </HoverCardContent>
        </HoverCard>
      ))}
    </div>
  )
}

Gecikme

Trigger üzerindeki delay ve closeDelay, işaretçinin kart açılmadan önce ne kadar durması gerektiğini ve ayrıldıktan sonra ne kadar kalacağını belirler. 600ms varsayılanı, işaretçi bir sayfayı geçerken kartların yanıp sönerek açılmasını önler.

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

export function HoverCardDelay() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <HoverCard>
        <HoverCardTrigger
          href="#"
          render={
            <Button variant="outline" nativeButton={false} render={<a />} />
          }
        >
          Default (600ms)
        </HoverCardTrigger>
        <HoverCardContent className="w-56">
          Waits long enough that passing the pointer over the link doesn’t open
          it.
        </HoverCardContent>
      </HoverCard>
      <HoverCard>
        <HoverCardTrigger
          href="#"
          delay={150}
          closeDelay={100}
          render={
            <Button variant="outline" nativeButton={false} render={<a />} />
          }
        >
          Fast (150ms)
        </HoverCardTrigger>
        <HoverCardContent className="w-56">
          Opens almost right away and closes quickly.
        </HoverCardContent>
      </HoverCard>
    </div>
  )
}

Trigger'ı, bir cümlenin içindekiler dahil herhangi bir bağlantı yapmak için render kullanın. Bir bağlantı iki satıra sardığında kart, üzerine geldiğiniz satıra sabitlenir.

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

export function HoverCardInline() {
  return (
    <p className="max-w-sm text-sm text-muted-foreground">
      Built on{" "}
      <HoverCard>
        <HoverCardTrigger
          href="https://base-ui.com"
          render={
            <a className="font-medium text-foreground underline decoration-border underline-offset-4 hover:decoration-foreground" />
          }
        >
          Base UI
        </HoverCardTrigger>
        <HoverCardContent className="w-60">
          Unstyled, accessible React primitives from the creators of Radix,
          Floating UI and Material UI.
        </HoverCardContent>
      </HoverCard>{" "}
      primitives, styled with Tailwind CSS and theme tokens, and ready to copy
      into your project.
    </p>
  )
}

Etkileşimli içerik

İşaretçiyi bağlantıdan karta taşıyın; kart açık kalır, böylece içindeki bağlantılara ve düğmelere tıklanabilir. Aralarındaki yol hoşgörülüdür; bu yüzden çapraz bir hareket kartı kapatmaz.

import { IconExternalLink, IconStar } from "@tabler/icons-react"

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

export function HoverCardRichContent() {
  return (
    <HoverCard>
      <HoverCardTrigger
        href="https://github.com/preetsuthar17"
        render={<Button variant="link" nativeButton={false} render={<a />} />}
      >
        hextaui/components
      </HoverCardTrigger>
      <HoverCardContent className="w-72">
        <div className="flex flex-col gap-2">
          <p className="font-medium">hextaui/components</p>
          <p className="text-muted-foreground">
            Copy-paste React components with motion, keyboard support and RTL
            built in.
          </p>
          <div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
            <span className="flex items-center gap-1">
              <IconStar className="size-3.5" aria-hidden="true" />
              2.4k
            </span>
            <a
              href="https://github.com/preetsuthar17"
              className="flex items-center gap-1 text-foreground underline-offset-4 hover:underline"
            >
              Open on GitHub
              <IconExternalLink className="size-3.5" aria-hidden="true" />
            </a>
          </div>
        </div>
      </HoverCardContent>
    </HoverCard>
  )
}

Paylaşılan kart

Tek bir kart birçok bağlantıya hizmet eder. createHoverCardHandle ile bir handle oluşturun, her trigger'a bir payload verin ve kartta okuyun. Adlar arasında hareket etmek, kartı kapatıp yeniden açmak yerine yeni bağlantıya kaydırır. Eski içerik hareket ettiğiniz yöne doğru kayarak çıkar, yeni içerik kayarak girer ve yükseklik ikisi arasında yumuşakça değişir.

"use client"

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

type Person = { name: string; initials: string; role: string }

const team: Person[] = [
  { name: "Ada Lovelace", initials: "AL", role: "Analyst" },
  { name: "Alan Turing", initials: "AT", role: "Research" },
  { name: "Grace Hopper", initials: "GH", role: "Compilers" },
]

const profileCard = createHoverCardHandle<Person>()

export function HoverCardDetached() {
  return (
    <div className="flex flex-col items-center gap-3">
      <ul className="flex flex-col items-start gap-2 text-sm">
        {team.map((person) => (
          <li key={person.name}>
            <HoverCardTrigger
              handle={profileCard}
              payload={person}
              href="#"
              delay={300}
              render={
                <a className="font-medium underline decoration-border underline-offset-4 hover:decoration-foreground" />
              }
            >
              {person.name}
            </HoverCardTrigger>
          </li>
        ))}
      </ul>
      <HoverCard handle={profileCard}>
        {({ payload }) => (
          <HoverCardContent side="inline-end" align="start" className="w-56">
            {payload ? (
              <div className="flex items-center gap-3">
                <Avatar>
                  <AvatarFallback>{payload.initials}</AvatarFallback>
                </Avatar>
                <div className="flex min-w-0 flex-col">
                  <p className="font-medium">{payload.name}</p>
                  <p className="text-muted-foreground">{payload.role}</p>
                </div>
              </div>
            ) : null}
          </HoverCardContent>
        )}
      </HoverCard>
    </div>
  )
}

Ok

arrow kartın kenarlığına dikişsiz birleşen bir işaretçi ekler. Taraf ofseti ona yer açmak için büyür ve kart ters döndüğünde onu izler.

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

export function HoverCardArrowDemo() {
  return (
    <HoverCard>
      <HoverCardTrigger
        href="#"
        delay={200}
        render={
          <Button variant="outline" nativeButton={false} render={<a />} />
        }
      >
        Release notes
      </HoverCardTrigger>
      <HoverCardContent arrow side="top" className="w-56">
        Version 2.4 adds shared hover cards and smoother text areas.
      </HoverCardContent>
    </HoverCard>
  )
}

İçerik yükleniyor

onOpenChange içinde veri çekmeye başlayın ve veri gelene kadar bir skeleton gösterin. İçerik değiştiğinde kart zıplamak yerine yeni yüksekliğine yumuşakça geçer.

"use client"

import * as React from "react"

import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
import { Skeleton, SkeletonText } from "@/components/ui/skeleton"

type Repo = { name: string; description: string; stars: number }

function fetchRepo(): Promise<Repo> {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve({
          name: "hextaui/hextaui",
          description:
            "Components built on shadcn/ui and Base UI, with motion, keyboard support and edge cases handled. Copy them into your project and make them yours.",
          stars: 2140,
        }),
      900
    )
  )
}

export function HoverCardAsync() {
  const [repo, setRepo] = React.useState<Repo | null>(null)
  const request = React.useRef<Promise<void> | null>(null)

  return (
    <HoverCard
      onOpenChange={(open) => {
        if (open && !request.current) {
          request.current = fetchRepo().then(setRepo)
        }
      }}
    >
      <HoverCardTrigger
        href="#"
        delay={200}
        render={
          <a className="text-sm font-medium underline decoration-border underline-offset-4 hover:decoration-foreground" />
        }
      >
        hextaui/hextaui
      </HoverCardTrigger>
      <HoverCardContent className="w-72" aria-busy={!repo}>
        {repo ? (
          <div className="flex flex-col gap-1.5">
            <p className="font-medium">{repo.name}</p>
            <p className="text-muted-foreground">{repo.description}</p>
            <p className="text-xs text-muted-foreground">
              {repo.stars.toLocaleString("en-US")} stars
            </p>
          </div>
        ) : (
          <div className="flex flex-col gap-2">
            <Skeleton className="h-4 w-32" />
            <SkeletonText lines={2} />
          </div>
        )}
      </HoverCardContent>
    </HoverCard>
  )
}

Kontrollü

open ve onOpenChange geçin. İkinci argüman, trigger-hover, trigger-focus veya escape-key gibi değişimin nedenini söyler.

"use client"

import * as React from "react"

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

export function HoverCardControlled() {
  const [open, setOpen] = React.useState(false)
  const [reason, setReason] = React.useState("none")

  return (
    <div className="flex flex-col items-center gap-3">
      <HoverCard
        open={open}
        onOpenChange={(next, details) => {
          setOpen(next)
          setReason(details.reason)
        }}
      >
        <HoverCardTrigger
          href="#"
          render={<Button variant="link" nativeButton={false} render={<a />} />}
        >
          Release notes
        </HoverCardTrigger>
        <HoverCardContent className="w-60">
          Version 2.0 rebuilds every component on Base UI.
        </HoverCardContent>
      </HoverCard>
      <p className="text-sm text-muted-foreground">
        Open: {String(open)} · last reason: {reason}
      </p>
    </div>
  )
}

Uzun içerik

Kırılmayan metin kartın içinde sarar ve trigger'ın yanındaki alandan uzun bir kart ekrandan çıkmak yerine kayar.

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

export function HoverCardLongContent() {
  return (
    <HoverCard>
      <HoverCardTrigger
        href="#"
        render={<Button variant="link" nativeButton={false} render={<a />} />}
      >
        Long preview
      </HoverCardTrigger>
      <HoverCardContent>
        <p className="font-medium">
          https://example.com/a/really/long/url/without/any/spaces/at/all
        </p>
        <p className="text-muted-foreground">
          Long previews wrap inside the card, and when the card is taller than
          the space around the trigger it scrolls instead of leaving the screen.
          Keep previews short, though: everything here should also be on the
          linked page.
        </p>
      </HoverCardContent>
    </HoverCard>
  )
}

Sağdan sola

Kart trigger'ın yönünü okur; bu yüzden mantıksal taraflar ve hizalama ters döner ve ölçek animasyonu doğru köşeden büyür.

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

export function HoverCardRtl() {
  return (
    <div dir="rtl">
      <HoverCard>
        <HoverCardTrigger
          href="#"
          render={<Button variant="link" nativeButton={false} render={<a />} />}
        >
          <span dir="ltr">@hextaui</span>
        </HoverCardTrigger>
        <HoverCardContent side="inline-end">
          <div className="flex gap-3">
            <Avatar>
              <AvatarFallback>هـ</AvatarFallback>
            </Avatar>
            <div className="flex min-w-0 flex-col gap-1">
              <p className="font-medium">هكستا</p>
              <p className="text-muted-foreground">
                مكونات مبنية على shadcn/ui مع حركة سلسة ودعم كامل للوحة
                المفاتيح.
              </p>
            </div>
          </div>
        </HoverCardContent>
      </HoverCard>
    </div>
  )
}
TuşAction
TabTrigger'a odaklanmak, üzerine gelmeyle aynı gecikmeden sonra kartı açar. Odağı başka yere taşımak kartı kapatır.
EnterDiğer bağlantılar gibi bağlantıyı izler.
EscKartı kapatır.
  • Kart, gören fare ve klavye kullanıcıları için görsel bir ektir. Ekran okuyucular yalnızca bağlantıyı duyar; böylece geçtikleri her bağlantıda bir önizlemeden geçmeye zorlanmazlar.
  • Üzerine gelmenin olmadığı dokunmatik ekranlarda hiçbir şey açılmaz. Bir dokunuş bağlantıyı izler; bu yüzden hedefin aynı bilgiyi içermesi gerekir.
  • Odak asla kartın içine geçmez. Klavyeyle erişilebilir olması gereken kontroller gerekiyorsa bunun yerine bir popover kullanın.
  • Azaltılmış hareket açıkken kart ölçeklenmeden solar ve paylaşılan kart bağlantılar arasında kaymak yerine atlar.

Base UI preview card üzerine kuruludur. Her parça, sardığı primitive'in prop'larını kabul eder.

PropTürVarsayılan
open
boolean–
defaultOpen
booleanfalse
onOpenChangedetails.reason, trigger-hover, trigger-focus, trigger-press, outside-press, escape-key, imperative-action veya none olur.
(open: boolean, details) => void–
onOpenChangeCompleteAçma ya da kapatma animasyonu bittikten sonra çağrılır.
(open: boolean) => void–
handleKökün dışında render edilen trigger'ları bağlar.
HoverCardHandle<Payload>–
childrenKartı açan trigger'ın payload'unu okumak için fonksiyon biçimini kullanın.
ReactNode | ({ payload }) => ReactNode–
actionsRef
RefObject<{ close, unmount }>–
PropTürVarsayılan
href
string–
delayÜzerine gelmenin veya odaklanmanın kartı açmasından önceki milisaniye.
number600
closeDelayAyrıldıktan sonra kartın açık kaldığı milisaniye.
number300
handle
HoverCardHandle<Payload>–
payloadBu trigger kartı açtığında karta geçirilir.
Payload–
render<Button variant="link" /> veya bir router bağlantısı gibi kendi bağlantınızı render edin.
ReactElement | (props, state) => ReactElement<a>
ÖznitelikAçıklama
data-slot="hover-card-trigger"CSS'te tetikleyiciyi hedefleyin.
data-popup-openBu trigger'ın kartı açıkken bulunur.
PropTürVarsayılan
side
"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end""bottom"
align
"start" | "center" | "end""center"
arrowTrigger'a doğru bir işaretçi gösterin.
booleanfalse
sideOffset
number | OffsetFunction6, or 10 with arrow
alignOffset
number | OffsetFunction0
collisionPaddingKart ile görünüm alanı kenarı arasında bırakılan boşluk.
number | Rect8
collisionAvoidanceKartın çarpışmada ters dönüp dönmediği, kayıp kaymadığı veya hiçbir şey yapıp yapmadığı.
CollisionAvoidance–
sticky
booleanfalse
anchorTrigger dışında bir şeye göre konumlandırın.
Element | RefObject | VirtualElement–
positionMethod
"absolute" | "fixed""absolute"
disableAnchorTracking
booleanfalse
portalPropscontainer gibi portal prop'ları.
HoverCardPortalProps–
render
ReactElement | (props, state) => ReactElement<div>
ÖznitelikAçıklama
data-slot="hover-card-content"CSS'te kartı hedefleyin.
data-openKart açıkken bulunur.
data-starting-styleKart açılma animasyonu sırasında bulunur.
data-ending-styleKart kapanma animasyonu sırasında bulunur.
data-instantKartı klavye odağı açtığında focus, Escape veya dış bir basış kapattığında dismiss. Ayarlıyken çıkış animasyonu atlanır.
data-sideÇarpışmalardan sonra kartın yerleştiği taraf.
data-alignYerleştiği hizalama.
--transform-originÖlçek animasyonunun büyüdüğü, trigger'ın yanındaki nokta.
--available-widthTrigger'ın yanında kalan yer. Kart asla bunu aşmaz.
--available-heightÜstte veya altta kalan yer. Daha uzun içerik kayar.
Positioner niteliğiAçıklama
data-slot="hover-card-positioner"Hareket eden öğe. Paylaşılan bir kart bağlantı değiştirdiğinde kayar.
data-anchor-hiddenTrigger görünümden çıktığında bulunur.
İç parçalarAçıklama
data-slot="hover-card-viewport"İçeriği sarar. Paylaşılan bir kart bağlantı değiştirirken data-activation-direction taşır.
data-slot="hover-card-body"İçeriğiniz. Değiştiğinde yüksekliği yumuşakça değişir.
data-slot="hover-card-arrow"Kenarı için data-side içeren işaretçi.
--popup-heightBağlantılar arasında yeniden boyutlanırken kartta ayarlanır.
PropTürVarsayılan
container
HTMLElement | ShadowRoot | RefObject | nulldocument.body
keepMounted
booleanfalse

Ayrılmış trigger'lar için bir handle döndürür. open(triggerId) ve close() yöntemleri kartı olay işleyicilerinden kontrol eder, isOpen state'ini okur. Payload'u tiplendirmek için bir tür argümanı geçin.

Kullanıldığı bloklar

Hover card üzerine kurulan bloklar.