דילוג לתוכן
uiify

Hover card

כרטיס עם תוכן לתצוגה מקדימה שנפתח בריחוף.

דפדפנים
Chrome 114+Firefox 125+Safari 17+
שכבה
1
גודל חבילה דחוסה
1.92 kB
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

Hover Card

Closed

UIify

Native-first React components with accessible semantics and optional styles.

Open-source · React 19 · Server compatible

Open

@uiify
UIify

Native-first React components with accessible semantics and optional styles.

Open-source · React 19 · Server compatible
פרטים נוספים מופיעים כאן.
import { HoverCard } from "@gunkin/uiify";

export default function Example() {
  return (
    <HoverCard.Root>
      <HoverCard.Trigger className="">@uiify</HoverCard.Trigger>
      <HoverCard.Content className="">Accessible React components that work before hydration.</HoverCard.Content>
    </HoverCard.Root>
  );
}
/* Button — pill-shaped and monochrome: a filled foreground action by default, secondary for
   the tonal one, outline for the bordered one, ghost for a surface that only appears on
   hover, link for inline text actions.

   Filled variants are lit from above by two layers that sit between the fill and the label:
   - `::before`, the wash: a blurred copy of the shape raised by half its height, so only its
     soft lower edge glows inside the upper half. `filter` runs before `clip-path`, which lets
     one box blur itself and still be cut back to the button outline.
   - `::after`, the rim: a half-pixel ring whose colour fades out towards the bottom. A
     box-shadow cannot take a gradient and `border-image` ignores the radius, so the ring is
     a gradient box with its content box masked out. */

@layer components {
  :where([data-uiify-button]) {
    --uiify-button-rim: rgb(255 255 255 / 20%);
    --uiify-button-wash: light-dark(rgb(255 255 255 / 15%), rgb(255 255 255 / 30%));

    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    min-block-size: var(--control-md);

    /* Never narrower than tall: an icon size only has to drop the padding to be a circle. */
    min-inline-size: var(--control-md);
    padding-block: 0;
    padding-inline: 1.25rem;
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-full);
    background-color: var(--foreground);
    background-clip: padding-box;
    color: var(--background);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    line-height: 1.5rem;
    text-decoration: none;
    cursor: pointer;
    transition: var(--transition-colors);
  }

  :where([data-uiify-button])::before,
  :where([data-uiify-button])::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    pointer-events: none;
  }

  :where([data-uiify-button])::before {
    background-color: var(--uiify-button-wash);
    filter: blur(10px);
    translate: 0 -50%;
    clip-path: inset(50% 0 -50% 0 round var(--radius-full));
  }

  :where([data-uiify-button])::after {
    padding: 0.5px;
    background-image: linear-gradient(to bottom, var(--uiify-button-rim), transparent 75%);
    mask:
      linear-gradient(#fff 0 0) content-box exclude,
      linear-gradient(#fff 0 0);
  }

  /* Sizes: md is the default metrics above. */
  [data-uiify-button]:is([data-size="sm"], [data-size="icon-sm"]) {
    min-block-size: var(--control-sm);
    min-inline-size: var(--control-sm);
    padding-inline: 0.75rem;
    gap: 0.25rem;
    font-size: var(--text-sm);
    line-height: 1.25rem;
  }

  [data-uiify-button]:is([data-size="lg"], [data-size="icon-lg"]) {
    min-block-size: var(--control-lg);
    min-inline-size: var(--control-lg);
    padding-inline: 1.75rem;
    font-size: var(--text-lg);
    line-height: 1.75rem;
  }

  /* Icon sizes: the same three heights with no padding, for a single icon. */
  [data-uiify-button][data-size^="icon"] {
    padding-inline: 0;
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 90%, var(--background));
    }
  }

  [data-uiify-button]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 80%, var(--background));
  }

  [data-uiify-button]:disabled,
  [data-uiify-button][data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Secondary: tonal low-emphasis fill. */
  [data-uiify-button][data-variant="secondary"] {
    --uiify-button-rim: light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 10%));
    --uiify-button-wash: light-dark(rgb(255 255 255 / 20%), rgb(255 255 255 / 5%));

    background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
    color: var(--foreground);
  }

  /* Outline: no fill and no wash, a hairline border in the text colour. Its rim is a full
     border-width ring laid over that border, so the top edge reads slightly stronger and
     fades into the plain border below. */
  [data-uiify-button][data-variant="outline"] {
    --uiify-button-rim: color-mix(in oklch, var(--foreground) 25%, transparent);

    border-color: color-mix(in oklch, var(--foreground) 20%, transparent);
    background-color: transparent;
    color: var(--foreground);
  }

  [data-uiify-button][data-variant="outline"]::before {
    content: none;
  }

  [data-uiify-button][data-variant="outline"]::after {
    inset: calc(-1 * var(--border-width));
    padding: var(--border-width);
  }

  [data-uiify-button][data-variant="outline"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
  }

  /* Ghost: text only; the tonal surface appears on hover. Flat, so no light layers. */
  [data-uiify-button][data-variant="ghost"] {
    background-color: transparent;
    color: var(--foreground);
  }

  [data-uiify-button][data-variant="ghost"]::before,
  [data-uiify-button][data-variant="ghost"]::after {
    content: none;
  }

  [data-uiify-button][data-variant="ghost"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
  }

  [data-uiify-button][data-variant="secondary"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 26%, transparent);
  }

  /* Destructive: irreversible or dangerous actions. */
  [data-uiify-button][data-variant="destructive"] {
    --uiify-button-wash: rgb(255 255 255 / 15%);

    background-color: var(--destructive);
    color: var(--destructive-foreground);
  }

  [data-uiify-button][data-variant="destructive"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--destructive), black 20%);
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button][data-variant="outline"]:hover:not(:disabled, [data-disabled]),
    [data-uiify-button][data-variant="ghost"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
    }

    [data-uiify-button][data-variant="secondary"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 20%, transparent);
    }

    [data-uiify-button][data-variant="destructive"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--destructive), black 12%);
    }
  }
}

@layer components {
  /* Selected: the chosen item of a set or a toggle that is on, filled with the brand colour.
     The state is read from whatever already carries it: the `selected` prop, a pressed toggle,
     the current page, or — for a label styled as a button — the checked native control right
     before it, so a toggle works with no JavaScript. */
  [data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]),
  :checked + [data-uiify-button] {
    --uiify-button-rim: rgb(255 255 255 / 20%);
    --uiify-button-wash: rgb(255 255 255 / 15%);

    border-color: transparent;
    background-color: var(--primary);
    color: var(--primary-foreground);
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]):hover:not(
        :disabled,
        [data-disabled]
      ),
    :checked:not(:disabled) + [data-uiify-button]:hover {
      background-color: color-mix(in oklch, var(--primary) 88%, var(--primary-foreground));
    }
  }

  /* A label styled as a button follows the native control it belongs to. */
  :disabled + [data-uiify-button] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  [data-uiify-button][hidden] {
    display: none;
  }
}

@layer components {
  /* Link: an inline text action with no surface, on a button or an anchor alike. */
  [data-uiify-button][data-variant="link"],
  [data-uiify-button][data-variant="link"]:active {
    padding-inline: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--primary);
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }
  [data-uiify-button][data-variant="link"] {
    min-inline-size: 0;
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button][data-variant="link"]:hover {
      padding-inline: 0;
      border: 0;
      border-radius: 0;
      background: transparent;
      color: var(--primary);
      text-decoration: underline;
      text-underline-offset: 0.2em;
    }
  }

  [data-uiify-button][data-variant="link"]::before,
  [data-uiify-button][data-variant="link"]::after {
    content: none;
  }

  [data-uiify-button][data-variant="link"]:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 3px;
  }
}

/* Popup, popover, and tooltip surfaces. */

@layer components {
  :where(
    [data-uiify-popover][data-part="surface"],
    [data-uiify-popup][data-part="content"],
    [data-uiify-hover-card][data-part="content"]
  ):where(:not([popover]), :popover-open) {
    display: flex;
  }

  :where(
    [data-uiify-popover][data-part="surface"],
    [data-uiify-popup][data-part="content"],
    [data-uiify-hover-card][data-part="content"]
  ) {
    flex-direction: column;
    gap: var(--space-sm);
    inline-size: max-content;
    max-inline-size: 18rem;
    padding: var(--space-md);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-lg);
    background-color: var(--popover);
    color: var(--popover-foreground);
    box-shadow: var(--shadow-popover);
  }

  :where(
    [data-uiify-popover][data-part="surface"],
    [data-uiify-popup][data-part="content"],
    [data-uiify-hover-card][data-part="content"]
  )[data-state="closed"] {
    display: none;
  }

  [data-uiify-tooltip]:where(:not([popover]), :popover-open) {
    display: block;
  }

  [data-uiify-tooltip] {
    inline-size: max-content;
    max-inline-size: 16rem;
    padding-block: var(--space-xs);
    padding-inline: var(--space-sm);

    /* Cancels the UA popover default (`border: solid`, currentColor) — this
       surface is borderless by design. */
    border: none;
    border-radius: var(--radius-sm);
    background-color: var(--foreground);
    color: var(--background);
    font-size: var(--text-xs);
    line-height: var(--leading-normal);
    box-shadow: var(--shadow-popover);
  }

  :where([data-uiify-tooltip])[data-state="closed"] {
    display: none;
  }

  @media (prefers-reduced-motion: no-preference) {
    :where(
      [data-uiify-popover][data-part="surface"],
      [data-uiify-popup][data-part="content"],
      [data-uiify-hover-card][data-part="content"],
      [data-uiify-tooltip]
    ) {
      transition: var(--transition-presence);
    }

    :where(
        [data-uiify-popover][data-part="surface"],
        [data-uiify-popup][data-part="content"],
        [data-uiify-hover-card][data-part="content"],
        [data-uiify-tooltip]
      )[popover]:not(:popover-open) {
      opacity: 0;
      scale: var(--presence-scale);
    }

    @starting-style {
      :where(
        [data-uiify-popover][data-part="surface"],
        [data-uiify-popup][data-part="content"],
        [data-uiify-hover-card][data-part="content"],
        [data-uiify-tooltip]
      ):popover-open {
        opacity: 0;
        scale: var(--presence-scale);
      }
    }
  }
}
import { HoverCard } from "@gunkin/uiify";

export default function Example() {
  return (
    <HoverCard.Root>
      <HoverCard.Trigger
        className={
          "relative isolate inline-flex items-center justify-center gap-1.5 min-h-10 min-w-10 px-5 py-px border-[len" +
          "gth:var(--border-width)] border-solid border-transparent rounded-full bg-clip-padding text-base font-medi" +
          "um leading-6 no-underline cursor-pointer [transition:var(--transition-colors)] focus-visible:[outline:non" +
          "e] focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)] d" +
          "isabled:opacity-50 disabled:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:cursor-not-allo" +
          "wed [&:is([data-selected],[aria-pressed=true],[aria-current=page])]:[--uiify-button-rim:rgb(255_255_255_/" +
          "_20%)] [&:is([data-selected],[aria-pressed=true],[aria-current=page])]:[--uiify-button-wash:rgb(255_255_2" +
          "55_/_15%)] [&:is([data-selected],[aria-pressed=true],[aria-current=page])]:border-transparent [&:is([data" +
          "-selected],[aria-pressed=true],[aria-current=page])]:bg-primary [&:is([data-selected],[aria-pressed=true]" +
          ",[aria-current=page])]:text-primary-foreground [:checked+&]:[--uiify-button-rim:rgb(255_255_255_/_20%)] [" +
          ":checked+&]:[--uiify-button-wash:rgb(255_255_255_/_15%)] [:checked+&]:border-transparent [:checked+&]:bg-" +
          "primary [:checked+&]:text-primary-foreground [@media_(any-hover:_hover)_and_(any-pointer:_fine)]:[&:is([d" +
          "ata-selected],[aria-pressed=true],[aria-current=page]):hover:not(:disabled,[data-disabled])]:[background-" +
          "color:color-mix(in_oklch,_var(--primary)_88%,_var(--primary-foreground))] [@media_(any-hover:_hover)_and_" +
          "(any-pointer:_fine)]:[:checked:not(:disabled)+&:hover]:[background-color:color-mix(in_oklch,_var(--primar" +
          "y)_88%,_var(--primary-foreground))] bg-transparent text-foreground before:content-none after:content-none" +
          " hover:[&:not(:disabled,[data-disabled])]:[background-color:color-mix(in_oklch,_var(--foreground)_10%,_tr" +
          "ansparent)] active:[&:not(:disabled,[data-disabled])]:[background-color:color-mix(in_oklch,_var(--foregro" +
          "und)_16%,_transparent)]"
        }
      >
        @uiify
      </HoverCard.Trigger>
      <HoverCard.Content
        className="
          [&[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)]
          [&_[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)]
          [&[popover]:popover-open:not([data-positioning=native])]:[margin:revert]
          [&_[popover]:popover-open:not([data-positioning=native])]:[margin:revert] flex-col gap-sm w-max max-w-72 p-md
          border-[length:var(--border-width)] border-solid border-border rounded-lg bg-popover text-popover-foreground
          shadow-popover [&:where(:not([popover]),:popover-open)]:flex data-[state=closed]:hidden
          [&[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)]
          motion-safe:[transition:var(--transition-presence)] motion-safe:[&:not(:popover-open)]:opacity-0
          motion-safe:[&:not(:popover-open)]:[scale:var(--presence-scale)]
          motion-safe:starting:[&:popover-open]:opacity-0
          motion-safe:starting:[&:popover-open]:[scale:var(--presence-scale)]
        "
      >
        Accessible React components that work before hydration.
      </HoverCard.Content>
    </HoverCard.Root>
  );
}

API

HoverCard.Root

מאפייןטיפוסברירת מחדלתיאור
openboolean—מצב פתיחה מבוקר.
defaultOpenbooleanfalseמצב פתיחה התחלתי לא מבוקר.
onOpenChangePopoverChangeHandler—נקראת עם מצב הפתיחה הבא ופרטי הגורם לשינוי.
openDelaynumber700מספר אלפיות השנייה שהמצביע צריך לנוח על כפתור הפתיחה לפני שהכרטיס נפתח.
closeDelaynumber300מספר אלפיות השנייה בין יציאת המצביע לסגירת הכרטיס, כדי לאפשר מעבר של המצביע אל הכרטיס.
idstring—מזהה בסיס לעוגן ולתוכן; נוצר אוטומטית כשהוא מושמט.
childrenReactNode—החלקים Trigger ו-Content.

HoverCard.Trigger

מאפייןטיפוסברירת מחדלתיאור
classNamestring—המחלקה המוחלת על אלמנט הפתיחה.
childrenReactNode—תוכן אלמנט הפתיחה.
asElementType"button"הצגת אלמנט או רכיב אחר במקום תגית ברירת המחדל.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

HoverCard.Content

מאפייןטיפוסברירת מחדלתיאור
side"top" | "right" | "bottom" | "left""bottom"הצד של כפתור הפתיחה שאליו הכרטיס מעוגן.
align"start" | "center" | "end""center"יישור הכרטיס לאורך הצד שנבחר.
classNamestring—המחלקה המוחלת על משטח הכרטיס.
childrenReactNode—תוכן הכרטיס.
asElementType"div"הצגת אלמנט או רכיב אחר במקום תגית ברירת המחדל.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

מקלדת

מקשפעולה
Tabהעברת המיקוד אל הפקד או ממנו
Enter / Spaceהפעלה כשהפקד ממוקד (פקדים דמויי כפתור)

נגישות

משתמש ב-HTML סמנטי ככל האפשר. בדקו את הסימון המרונדר במגרש הניסויים — ARIA נוסף רק כאשר האלמנטים המובנים אינם מספיקים.

נקודות חיבור לעיצוב

data-disabled
דף זה תורגם אוטומטית וטרם נבדק.