Preview Card
A hover and focus popup that shows a visual preview for a link destination.
API Reference
Original primitive API
Behavior, accessibility details, and low-level props are documented by Base UI.
Basic
The principles of good typography remain in the digital age.
import { PreviewCard, PreviewCardTrigger, PreviewCardContent } from "moduix";const typographyUrl = "https://en.wikipedia.org/wiki/Typography";const typographyImageUrl = "/images/typography-preview.jpg";export function PreviewCardDemo() { return ( <PreviewCard> <p className={styles.paragraph}> The principles of good{" "} <PreviewCardTrigger href={typographyUrl}>typography</PreviewCardTrigger>{" "} remain in the digital age. </p> <PreviewCardContent> <div className={styles.popupContent}> <img className={styles.image} alt="Preview illustration for Typography" src={typographyImageUrl} /> <p className={styles.summary}> <strong>Typography</strong> <br /> Typography is the art and technique of arranging type to make written language readable and expressive. </p> </div> </PreviewCardContent> </PreviewCard> );}.paragraph { max-width: 33rem; margin: 0; color: var(--color-foreground); font-size: var(--text-md); line-height: var(--line-height-text-md);}.popupContent { display: grid; gap: var(--spacing-2);}.image { display: block; width: 14rem; max-width: min(14rem, calc(100vw - 4rem)); border-radius: var(--radius-sm); object-fit: cover;}.summary { max-width: 14rem; margin: 0; color: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm);}Full list of component variables available for project-level overrides.
| Property | Default | Description |
|---|---|---|
| --preview-card-arrow-height | 0.625rem | Controls the default arrow SVG height. |
| --preview-card-arrow-inline-offset | 0.8125rem | Controls the inline-axis arrow offset. |
| --preview-card-arrow-size | 0.5rem | Controls the block-axis arrow offset. |
| --preview-card-arrow-stroke-color | var(--preview-card-border-color) | Controls arrow border color. |
| --preview-card-arrow-width | 1.25rem | Controls the default arrow SVG width. |
| --preview-card-backdrop-bg | var(--backdrop-bg, transparent) | Controls the backdrop color. |
| --preview-card-backdrop-blur | 0 | Controls the backdrop blur. |
| --preview-card-backdrop-transition | var(--transition-default) | Controls the backdrop transition. |
| --preview-card-bg | var(--color-popover) | Controls the popup background color. |
| --preview-card-border-color | var(--color-border) | Controls the popup border color. |
| --preview-card-border-width | var(--border-width-sm) | Controls popup border width. |
| --preview-card-color | var(--color-popover-foreground) | Controls the popup text color. |
| --preview-card-disabled-opacity | var(--opacity-disabled) | Controls disabled opacity. |
| --preview-card-focus-ring-color | var(--color-ring) | Controls the trigger focus ring color. |
| --preview-card-focus-ring-width | var(--border-width-sm) | Controls focus ring width. |
| --preview-card-height | auto | Controls the popup height. |
| --preview-card-max-height | 24rem | Controls the popup max height. |
| --preview-card-max-width | 24rem | Controls the popup max width. |
| --preview-card-min-width | 14rem | Controls the popup min width. |
| --preview-card-padding-x | var(--spacing-2) | Controls the popup horizontal padding. |
| --preview-card-padding-y | var(--spacing-2) | Controls the popup vertical padding. |
| --preview-card-radius | var(--radius-lg) | Controls the popup border radius. |
| --preview-card-scale | var(--scale-popup) | Controls the popup enter and exit scale. |
| --preview-card-shadow | var(--shadow-lg) | Controls the popup shadow. |
| --preview-card-transition | var(--transition-default) | Controls popup and trigger transitions. |
| --preview-card-trigger-color | var(--color-primary) | Controls the default trigger text color. |
| --preview-card-trigger-decoration-color | color-mix(in oklab, var(--preview-card-trigger-color), transparent 40%) | Controls the trigger underline color. |
| --preview-card-trigger-decoration-color-hover | var(--preview-card-trigger-color) | Controls the trigger underline color on hover. |
| --preview-card-trigger-decoration-color-open | var(--preview-card-trigger-color) | Controls the trigger underline color while the popup is open. |
| --preview-card-trigger-decoration-thickness | 1px | Controls the trigger underline thickness. |
| --preview-card-trigger-focus-offset | 1px | Controls the trigger focus outline offset. |
| --preview-card-trigger-focus-radius | var(--radius-xs) | Controls the trigger focus radius. |
| --preview-card-trigger-underline-offset | 2px | Controls the trigger underline offset. |
| --preview-card-width | auto | Controls the popup width. |
Interactive variables scoped for docs preview without changing size scale tokens.
| Property | Value | Default | Description |
|---|---|---|---|
| --preview-card-backdrop-bg | var(--backdrop-bg, transparent) | Controls backdrop color. | |
| --preview-card-bg | var(--color-popover) | Controls popup background color. | |
| --preview-card-border-color | var(--color-border) | Controls popup border color. | |
| --preview-card-color | var(--color-popover-foreground) | Controls popup text color. | |
| --preview-card-focus-ring-color | var(--color-ring) | Controls trigger focus ring color. | |
| --preview-card-radius | var(--radius-lg) | Controls popup border radius. | |
| --preview-card-shadow | var(--shadow-lg) | Controls popup shadow. | |
| --preview-card-trigger-color | var(--color-primary) | Controls trigger text color. |
Anatomy
PreviewCard pairs one or more triggers with one popup. Consumers compose only the root state,
visible trigger, and content; PreviewCardContent renders the portal, optional backdrop,
positioner, popup, arrow, and viewport internally.
Preview cards are a visual enhancement for hover and focus. Keep essential information available on the linked destination or in nearby page content, because touch and screen reader users may not receive the same popup preview.
PreviewCard
├─ PreviewCardTrigger
└─ PreviewCardContent
├─ portal (internal)
├─ backdrop (internal, optional)
├─ positioner (internal)
├─ popup
│ ├─ arrow (internal, optional)
│ └─ viewport (internal)
│ └─ content
└─ placement state<PreviewCard>
<PreviewCardTrigger href="https://en.wikipedia.org/wiki/Typography">
typography
</PreviewCardTrigger>
<PreviewCardContent>
<div className={styles.popupContent}>Preview content</div>
</PreviewCardContent>
</PreviewCard>| Part | Role |
|---|---|
PreviewCard | Root state machine. Controls open state, delays, payload, and detached trigger handles. |
PreviewCardTrigger | Interactive link or trigger. It opens the preview on hover or focus and receives open/disabled state data. |
PreviewCardContent | Public popup slot. It owns placement props, popup styling, optional arrow, backdrop, and service slots. |
portal | Internal service layer that moves the popup to document.body or the supplied container. |
positioner | Internal service layer that measures the trigger and writes placement variables. |
backdrop | Internal optional overlay enabled with withBackdrop. |
viewport | Internal transition container for content changes between payload-driven triggers. |
Style the popup with className on PreviewCardContent. Use classNames only for internal
service slots such as backdrop, positioner, arrow, and viewport when those slots need
targeted styling.
Composition
Use PreviewCard for root behavior props such as open, defaultOpen, onOpenChange,
delay, closeDelay, and handle. Use PreviewCardTrigger for the link itself; detached or
shared triggers can be connected with createPreviewCardHandle.
PreviewCardContent owns the rendered popup and its service slots. Pass placement props such as
side, align, sideOffset, alignOffset, collisionBoundary, and collisionPadding directly
to PreviewCardContent. Use container to choose the portal container, withBackdrop to enable
the optional backdrop, withArrow={false} to remove the default arrow, and arrow to replace it.
Use slotProps only when you need to pass Base UI escape-hatch props to an internal slot, such as
keepMounted on the portal or a custom render for the viewport.
Examples
Controlled
Control the open state from React when the preview needs to coordinate with other UI.
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from "moduix";import { useState } from "react";export function ControlledPreviewCard() { const [open, setOpen] = useState(false); const typographyUrl = "https://en.wikipedia.org/wiki/Typography"; return ( <PreviewCard open={open} onOpenChange={setOpen}> <PreviewCardTrigger href={typographyUrl}>Controlled preview card</PreviewCardTrigger> <PreviewCardContent> <div className={styles.popupContent}> <p className={styles.summary}> This preview card is controlled with open and onOpenChange. </p> </div> </PreviewCardContent> </PreviewCard> );}.popupContent { display: grid; gap: var(--spacing-2);}.summary { max-width: 14rem; margin: 0; color: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm);}Detached Trigger
Use createPreviewCardHandle when the trigger and popup content live in different parts of the tree.
import { createPreviewCardHandle, PreviewCardTrigger, PreviewCard, PreviewCardContent } from "moduix";import { useMemo } from "react";export function DetachedTriggerPreviewCard() { const previewCardHandle = useMemo(() => createPreviewCardHandle(), []); const typographyUrl = "https://en.wikipedia.org/wiki/Typography"; return ( <div className={styles.row}> <PreviewCardTrigger handle={previewCardHandle} href={typographyUrl}> Open detached preview </PreviewCardTrigger> <PreviewCard handle={previewCardHandle}> <PreviewCardContent> <div className={styles.popupContent}> <p className={styles.summary}> Trigger and popup are linked with createPreviewCardHandle(). </p> </div> </PreviewCardContent> </PreviewCard> </div> );}.popupContent { display: grid; gap: var(--spacing-2);}.summary { max-width: 14rem; margin: 0; color: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm);}.row { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--spacing-2);}Multiple Triggers With Payload
Share one popup between several links and pass trigger-specific data through payload.
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger,} from "moduix";import { useMemo } from "react";type LinkPreviewPayload = { title: string; url: string; summary: string;};export function MultipleTriggersPreviewCard() { const previewCardHandle = useMemo(() => createPreviewCardHandle(), []); return ( <div className={styles.row}> {items.map((item) => ( <PreviewCardTrigger key={item.title} handle={previewCardHandle} href={item.url} payload={item} > {item.title} </PreviewCardTrigger> ))} <PreviewCard handle={previewCardHandle}>{renderPreview}</PreviewCard> </div> ); function renderPreview({ payload }) { const item = (payload as LinkPreviewPayload | undefined) ?? items[0]; return ( <PreviewCardContent> <div className={styles.popupContent}> <p className={styles.summary}> <strong>{item.title}</strong> <br /> {item.summary} </p> </div> </PreviewCardContent> ); }}.popupContent { display: grid; gap: var(--spacing-2);}.summary { max-width: 14rem; margin: 0; color: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm);}.row { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--spacing-2);}const items: LinkPreviewPayload[] = [ { title: "Typography", url: "https://en.wikipedia.org/wiki/Typography", summary: "Typography is the art and technique of arranging type.", }, { title: "Grid systems", url: "https://en.wikipedia.org/wiki/Grid_(graphic_design)", summary: "Grid systems help organize content and spacing.", },];Side Control
Pass side, align, offsets, collision options, or slotProps.positioner to
PreviewCardContent when you need to control popup placement.
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from "moduix";import { useState } from "react";export function SideControlPreviewCard() { const [side, setSide] = useState("bottom" as (typeof sides)[number]); const typographyUrl = "https://en.wikipedia.org/wiki/Typography"; return ( <div className={styles.stack}> <div className={styles.sideButtons}> {sides.map((item) => ( <button key={item} type="button" className={styles.sideButton} data-active={item === side || undefined} onClick={() => setSide(item)} > {item} </button> ))} </div> <PreviewCard> <PreviewCardTrigger href={typographyUrl}>Placement: {side}</PreviewCardTrigger> <PreviewCardContent side={side} className={styles.sidePopup}> <div className={styles.popupContent}> <p className={styles.summary}>Current side is {side}.</p> </div> </PreviewCardContent> </PreviewCard> </div> );}.popupContent { display: grid; gap: var(--spacing-2);}.summary { max-width: 14rem; margin: 0; color: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm);}.stack { display: grid; justify-items: center; gap: var(--spacing-3);}.sideButtons { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-1); padding: var(--spacing-1); border: var(--border-width-sm) solid var(--color-border); border-radius: var(--radius-md); background: var(--color-muted);}.sideButton { min-width: 4.75rem; min-height: 2rem; padding: 0 var(--spacing-3); border: var(--border-width-sm) solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-background); color: var(--color-foreground); font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium); line-height: var(--line-height-text-sm); text-transform: capitalize; cursor: pointer; transition: background-color var(--transition-default), color var(--transition-default); &[data-active] { background: var(--color-primary); color: var(--color-primary-foreground); }}.sidePopup { --preview-card-max-width: 17rem;}const sides = ["top", "right", "bottom", "left"] as const;Custom Arrow
Pass arrow to PreviewCardContent to render any custom icon or SVG.
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from "moduix";export function CustomArrowPreviewCard() { return ( <PreviewCard> <PreviewCardTrigger href="https://en.wikipedia.org/wiki/Typography"> Preview with custom arrow </PreviewCardTrigger> <PreviewCardContent className={styles.customArrowPopup} arrow={<CustomArrow />}> <div className={styles.popupContent}> <p className={styles.summary}> Pass any React node to the arrow prop to use a custom SVG or icon. </p> </div> </PreviewCardContent> </PreviewCard> );}function CustomArrow() { return ( <svg className={styles.customArrow} viewBox="0 0 16 8" fill="none" aria-hidden="true"> <path d="M8 0L16 8H0L8 0Z" fill="currentColor" /> </svg> );}.popupContent { display: grid; gap: var(--spacing-2);}.summary { max-width: 14rem; margin: 0; color: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm);}.customArrowPopup { --preview-card-bg: var(--color-foreground); --preview-card-color: var(--color-background); --preview-card-border-color: color-mix(in oklab, var(--color-foreground), transparent 25%);}.customArrow { display: block; width: 1rem; height: 0.5rem; color: var(--preview-card-bg, var(--color-popover));}Custom Styles
PreviewCardContent renders the portal, backdrop, positioner, and viewport internally. Use
classNames and slotProps when you need to style or configure those internal slots.
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from "moduix";export function SlotCustomizationPreviewCard() { return ( <PreviewCard> <PreviewCardTrigger className={styles.slotTrigger} href="https://en.wikipedia.org/wiki/Typography"> Preview with styled slots </PreviewCardTrigger> <PreviewCardContent withBackdrop classNames={{ backdrop: styles.backdrop, arrow: styles.slotArrow, viewport: styles.viewport, }} > <div className={styles.popupContent}> <p className={styles.summary}> Portal, backdrop, positioner, and viewport are rendered by PreviewCardContent. </p> </div> </PreviewCardContent> </PreviewCard> );}.popupContent { display: grid; gap: var(--spacing-2);}.summary { max-width: 14rem; margin: 0; color: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm);}.backdrop { --preview-card-backdrop-bg: color-mix(in oklab, var(--color-background), transparent 70%); --preview-card-backdrop-blur: 2px; --preview-card-backdrop-transition: 200ms ease;}.slotTrigger { position: relative; z-index: calc(var(--z-popup) + 1);}.slotArrow { --preview-card-arrow-stroke-color: var(--preview-card-border-color);}.viewport { padding: var(--spacing-1);}