FairGarden Design

Preview Card

A hover preview of a linked page: a thumbnail, the page's title, one line of description and its domain.

import {
  PreviewCard,
  PreviewCardArrow,
  PreviewCardDescription,
  PreviewCardDomain,
  PreviewCardPopup,
  PreviewCardThumb,
  PreviewCardTitle,
  PreviewCardTrigger,
} from '@fairgarden-private/design/components/PreviewCard'

The trigger is a Link, so it navigates and prints like any link. The card opens after 600 ms of hover and closes 300 ms after the pointer leaves. It appears only with hover and a fine pointer; on touch the link simply navigates. The card is the same anchored white panel as a Popover, with a --border-size-2 --primary12 frame, and it is hidden in print.

A preview card holds at most one link and is never the only route to its information.

A link preview

PreviewCardThumb is a 16:9 image with the nested radius; give it alt, or an empty alt when the title already names it. The title is set in type-itemhead and the domain in mono.

From the saddle, the loop meets the Ridge Connector and drops to the reservoir. Hover the link with a mouse to preview it; on touch it simply navigates.

PreviewCardBasic.tsx
'use client'

import {
  PreviewCard,
  PreviewCardArrow,
  PreviewCardDescription,
  PreviewCardDomain,
  PreviewCardPopup,
  PreviewCardThumb,
  PreviewCardTitle,
  PreviewCardTrigger,
} from '@fairgarden-private/design/components/PreviewCard'
import styles from './basic.module.css'

export function PreviewCardBasic() {
  return (
    <p className={styles.text}>
      From the saddle, the loop meets the{' '}
      <PreviewCard>
        <PreviewCardTrigger href="https://example.org/trails/ridge-connector">
          Ridge Connector
        </PreviewCardTrigger>
        <PreviewCardPopup>
          <PreviewCardArrow />
          <PreviewCardThumb
            render={
              <svg viewBox="0 0 160 90" role="img" aria-label="Map sketch of the Ridge Connector">
                <path className={styles.ridge} d="M0 72 40 44l30 16 40-36 50 34" />
                <path className={styles.trail} d="M12 78c30-10 44-30 70-30s40-18 66-28" />
              </svg>
            }
            className={styles.thumb}
          />
          <PreviewCardTitle>Ridge Connector</PreviewCardTitle>
          <PreviewCardDescription>A 2.1 km link from the saddle to the reservoir dam.</PreviewCardDescription>
          <PreviewCardDomain>example.org</PreviewCardDomain>
        </PreviewCardPopup>
      </PreviewCard>{' '}
      and drops to the reservoir. Hover the link with a mouse to preview it; on touch it simply
      navigates.
    </p>
  )
}

API Reference

PreviewCard and PreviewCardTrigger

Groups the parts of a preview card (Base UI PreviewCard.Root).

The link that opens the card on hover. Renders a Link (§9.3), inline by default, so it navigates and prints like any link [D79]. The card is never the only route to its information.

PropTypeDescription
external
boolean | null | undefined

Adds the arrow-open mark and “(external site)” for assistive technology. Default false.

handle
PreviewCardHandle<unknown> | undefined

Associates a detached trigger with a PreviewCard created by PreviewCard.createHandle.

index
boolean | null | undefined

Adds the screen-only visited ✓ used in long indexes, such as reference and archive lists. Default false [D174, D175].

kind
| 'nav'
| 'title'
| 'inline'
| 'standalone'
| 'backref'
| 'noteref'
| null
| undefined

Which build: inline (default) underlines in running text; standalone is the caps module link with a trailing ›; nav has no rest underline and marks the current page; title stretches over a card or list item; noteref and backref are the note call and return (§9.3).

list
boolean | null | undefined

With kind="nav": a list link (nav-panel, footer, drawer and breadcrumb lists), whose hover is --role-link-hover color only, plus the --ds-stroke-1-5 --role-accent underline where that ink is --primary12. Without it, nav is bare navigation text (bar and utility items, page numbers, toolbar links), whose hover is the --border-size-2 --role-accent underline (§9.3) [D181]. Default false.

muted
boolean | null | undefined

Rests in --role-muted instead of --primary12, as a breadcrumb’s ancestors. Hover takes --role-link-hover only (the underline is added where that ink is --primary12); with kind="nav" it replaces the bar item’s bare-text underline (§9.3, §9.8) [D181]. Default false.

payload
unknown | undefined

A payload handed to the PreviewCard’s children function when this trigger opens it.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: the link text and focus ring. Never defaulted; omitted, it inherits the scope [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: the accent underline. Never defaulted; omitted, it inherits the scope.

delay
number | undefined

Hover time before the card opens, in ms. Default 600.

closeDelay
number | undefined

Time after the pointer leaves before the card closes, in ms. Default 300.

className
string | undefined

Extra class names, added after the module’s own.

PreviewCardPopup and PreviewCardArrow

The preview panel, rendered in its Base UI Portal as a nested white scope: --primary1 face, --border-size-2 --primary12 frame, --ds-radius-8, 240–360 px wide. Compose PreviewCardThumb, PreviewCardTitle, PreviewCardDescription and PreviewCardDomain inside it, and PreviewCardArrow for the tail. It holds at most one link.

PropTypeDescription
primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale inside the popup’s white scope. Omitted, the white preset’s default [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale inside the popup: accents only.

container
| HTMLElement
| ShadowRoot
| React.RefObject<HTMLElement | ShadowRoot | null>
| null
| undefined

The element the portal renders into. Default: document.body.

align
Align | undefined

Alignment to the link. Default start.

side
Side | undefined

Which side of the link the card sits on. Default bottom; it flips when there is no room.

sideOffset
number | OffsetFunction | undefined

Distance from the link in px. Default 8 (--size-px-2).

collisionPadding
Padding | undefined

Clearance from the viewport edge in px. Default 16.

The tail: a --ds-space-12 × 6 px triangle whose --border-size-2 edge continues the frame.

PreviewCardThumb, PreviewCardTitle, PreviewCardDescription and PreviewCardDomain

The page’s thumbnail: a --ratio-widescreen photograph with the nested radius (--ds-radius-8 − --size-px-3, floored at --radius-1) [D40]. Give it alt (empty when the title already names it).

The linked page’s title, in type-itemhead --primary12.

A one-line description, in type-body-ui --primary12.

The page’s domain, in type-data --role-muted (mono measures).

Additional types

previewCard
PreviewCardArrowProps

Props for PreviewCardArrow: Base UI PreviewCard.Arrow props.

type PreviewCardArrowProps = PreviewCardArrow.PreviewCardArrowProps
PreviewCardDescriptionProps

Props for PreviewCardDescription: p props and render.

type PreviewCardDescriptionProps = useRender.ComponentProps<'p'>
PreviewCardDomainProps

Props for PreviewCardDomain: p props and render.

type PreviewCardDomainProps = useRender.ComponentProps<'p'>
PreviewCardPopupProps

Props for PreviewCardPopup: Base UI PreviewCard.Popup props plus placement, color axes and portal options.

type PreviewCardPopupProps = PreviewCardPopupProps & {
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
  side?: Side;
  align?: Align;
  sideOffset?: number | OffsetFunction;
  collisionPadding?: Padding;
  container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null;
}
PreviewCardProps

Props for PreviewCard: Base UI PreviewCard.Root props (open, onOpenChange …).

type PreviewCardProps<Payload = unknown> = PreviewCardRoot.Props<Payload>
PreviewCardThumbProps

Props for PreviewCardThumb: img props and render.

type PreviewCardThumbProps = useRender.ComponentProps<'img'>
PreviewCardTitleProps

Props for PreviewCardTitle: p props and render.

type PreviewCardTitleProps = useRender.ComponentProps<'p'>
PreviewCardTriggerProps

Props for PreviewCardTrigger: Link props plus the hover delays and Base UI’s trigger options.

type PreviewCardTriggerProps = {
  /**
   * Which build: `inline` (default) underlines in running text; `standalone`
   * is the caps module link with a trailing ›; `nav` has no rest underline
   * and marks the current page; `title` stretches over a card or list item;
   * `noteref` and `backref` are the note call and return (§9.3).
   */
  kind?: 'nav' | 'title' | 'inline' | 'standalone' | 'backref' | 'noteref' | null;
  /**
   * Adds the screen-only visited ✓ used in long indexes, such as reference
   * and archive lists. Default `false` [D174, D175].
   */
  index?: boolean | null;
  /**
   * Adds the arrow-open mark and "(external site)" for assistive technology.
   * Default `false`.
   */
  external?: boolean | null;
  /**
   * With `kind="nav"`: a list link (nav-panel, footer, drawer and breadcrumb
   * lists), whose hover is `--role-link-hover` color only, plus the
   * `--ds-stroke-1-5` `--role-accent` underline where that ink is
   * `--primary12`. Without it, `nav` is bare navigation text (bar and
   * utility items, page numbers, toolbar links), whose hover is the
   * `--border-size-2` `--role-accent` underline (§9.3) [D181]. Default
   * `false`.
   */
  list?: boolean | null;
  /**
   * Rests in `--role-muted` instead of `--primary12`, as a breadcrumb's
   * ancestors. Hover takes `--role-link-hover` only (the underline is added
   * where that ink is `--primary12`); with `kind="nav"` it replaces the bar
   * item's bare-text underline (§9.3, §9.8) [D181]. Default `false`.
   */
  muted?: boolean | null;
  /**
   * Primary Radix scale: the link text and focus ring. Never defaulted;
   * omitted, it inherits the scope [D133].
   */
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /**
   * Secondary Radix scale: the accent underline. Never defaulted; omitted,
   * it inherits the scope.
   */
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
  /** Extra class names, added after the module's own. */
  className?: string;
  /** Hover time before the card opens, in ms. Default 600. */
  delay?: number;
  /** Time after the pointer leaves before the card closes, in ms. Default 300. */
  closeDelay?: number;
  /** Associates a detached trigger with a PreviewCard created by `PreviewCard.createHandle`. */
  handle?: PreviewCardHandle<unknown>;
  /** A payload handed to the PreviewCard's children function when this trigger opens it. */
  payload?: unknown;
}

Specification: DESIGN-SYSTEM.md §10.15 (preview card), §9.3 (link) and §7.6 (link notes in print).