Noctis

Preview card

A hover/focus link-preview card: an inline Trigger link opens a floating, elevated Popup after a short intent delay — and instantly on keyboard focus — showing a richer preview of what the link points to. Flush against the anchor, never modal, and built on Base UI for hover-intent timing and dismissal.

Basic

A PreviewCard.Trigger is an inline link that styles itself — an accent link that underlines on hover/focus, shows a focus-visible ring, and keeps a persistent underline while its card is open (no hand-authored classes). Hovering it (after a short intent delay) or focusing it from the keyboard opens the PreviewCard.Popup. The popup is elevated and flush — there is no arrow — and the page stays interactive beneath it.

Built on @noctis, the Stridge design language.

Rich content

Lay the card out with the thin content slots: PreviewCard.Media (a fixed-ratio thumbnail/OG-image frame), PreviewCard.Title, PreviewCard.Description, and PreviewCard.Meta (a subtle stats row). The popup stacks them in a column with a consistent gap and caps between a sensible min and max width; a card taller than the viewport caps at the available height and scrolls inside.

See the latest from Stridge.

Image loading

PreviewCard.Media reserves its aspect ratio up front, so the card never reflows as a thumbnail loads. Pair it with a skeleton fill while the image decodes and an onError fallback for broken images.

A preview with an image.

Hover timing

Hover-intent is a first-class knob. PreviewCard.Trigger opens on hover after delay (default 500 ms) and closes after closeDelay (default 200 ms) — a touch snappier than the platform norm — with a small grace area (the tight sideOffset of 4) so the pointer can travel from the trigger to the card without it closing. Keyboard focus opens the card instantly, with no scale animation. Tune delay / closeDelay per trigger.

Hover-intent is a knob. A snappy card opens the moment you arrive; a patient card waits until you mean it.

One card, many triggers

Several inline triggers can share a single card. Give each PreviewCard.Trigger a payload, read the active one from PreviewCard.Root's render-function child, and the card swaps to that trigger's content as you move between them — the canonical "paragraph of mentions" pattern.

The pioneers — @ada, @grace, and @katherine — all share one preview card.

Positioning

The card opens below the trigger by default, flipping above when there's no room. Steer it with side and align (and sideOffset / alignOffset) on PreviewCard.Popup — alignment is direction-aware, so it mirrors under RTL. collisionPadding (default 8) keeps the card off the viewport edges.

Hover this link to see the card open above it.

Keyboard

KeyAction
TabMove focus to the trigger link — which opens the preview card instantly (no open animation).
EscClose the preview card; focus stays on the trigger.
EnterFollow the trigger link (the card is supplementary — the link itself still navigates).

The card opens on keyboard focus of the trigger, so it is reachable without a pointer. Because it is non-modal and supplementary, focus is not trapped inside it — the preview enriches the link without interrupting the reading flow.

Anatomy

Compose a preview card from its parts. PreviewCard.Root owns the open state (it accepts every Base UI PreviewCard.Root prop — open, defaultOpen, onOpenChange — and a render-function child that receives the active trigger's payload).

  • PreviewCard.Root — owns the open state; renders no element of its own.
  • PreviewCard.Trigger — the inline link that opens the card, and styles its own accent affordance. Renders an <a>; opens on hover after delay ms (default 500, with a closeDelay grace period on leave, default 200) and instantly on keyboard focus. Compose your own anchor through render.
  • PreviewCard.Popup — the floating, elevated, flush surface holding the preview. Props: side (default collision-aware), align (default center), sideOffset (default 4), alignOffset, collisionPadding (default 8), density (comfortable / compact). Caps at the available height; its content scrolls in an inner viewport.
  • PreviewCard.Media — a fixed-aspect-ratio frame for a thumbnail or OG image; reserves its space and clips overflow.
  • PreviewCard.Title — the card heading (lifts to the foreground role at medium weight).
  • PreviewCard.Description — supporting copy (inherits the popup's secondary tone).
  • PreviewCard.Meta — a subtle flex row for stats or label/value pairs.

Every rendered part carries a data-slot (preview-card-trigger, preview-card-popup, preview-card-viewport, preview-card-media, preview-card-title, preview-card-description, preview-card-meta) for host-side styling — pair it with the Base UI state attributes (data-popup-open, data-side, data-starting-style, data-ending-style, data-instant).

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every preview card in that region retunes — e.g. .marketing { --noctis-preview-card-popup-max-width: 24rem; } widens every card opened beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that only forward to Base UI list just the props they pass through.

PreviewCard.Root

No props of its own — forwards to the underlying Base UI part.

PreviewCard.Trigger

Prop

PreviewCard.Popup

Prop

AttributeDescription
data-slotThe trigger element (and every rendered part).
data-popup-openPresent on the trigger while its preview card is open.
data-sideThe side of the anchor the popup actually rendered on (`bottom`, `top`, …).
data-starting-stylePresent on the popup for the first frame after mount — the transition's start state.
data-ending-stylePresent on the popup while it transitions out before unmounting.
data-instantPresent on the popup when the change should not animate (focus open, dismissal).