Skip to content
+

Preview Card

Preview Cards are visual-only interactive floating elements that display a concise preview of a link's contents when a user hovers over it before navigating through.

Installation

Base UI components are all available as a single package.

npm install @base_ui/react

Once you have the package installed, import the component.

import * as PreviewCard from '@base_ui/react/PreviewCard';

Anatomy

Preview Card is implemented using a collection of related components:

  • <PreviewCard.Root /> is a top-level component that wraps the other components.
  • <PreviewCard.Trigger /> renders the trigger element.
  • <PreviewCard.Backdrop /> renders an optional backdrop element behind the popup.
  • <PreviewCard.Positioner /> renders the Preview Card's positioning element.
  • <PreviewCard.Popup /> renders the Preview Card popup itself.
  • <PreviewCard.Arrow /> renders an optional pointing arrow, placed inside the popup.
<PreviewCard.Root>
  <PreviewCard.Trigger />
  <PreviewCard.Backdrop />
  <PreviewCard.Positioner>
    <PreviewCard.Popup>
      <PreviewCard.Arrow />
    </PreviewCard.Popup>
  </PreviewCard.Positioner>
</PreviewCard.Root>

Accessibility

Preview Cards are a type of progressive enhancement component to display a concise preview of a link before a sighted user decides to commit to navigating to the link's new location.

Guidelines:

  • Preview Cards should not contain content that cannot be viewed when navigating through to the link's new location — ensure they act as a preview of the same information contained in the link.
  • Preview Cards should avoid form inputs other than buttons, as the Preview Card will close when the cursor leaves its boundaries, making it difficult to preserve state or fill out information.

Placement

By default, the Preview Card is placed on the bottom side of its trigger, the default anchor. To change this, use the side prop:

<PreviewCard.Root>
  <PreviewCard.Trigger />
  <PreviewCard.Positioner side="right">
    <PreviewCard.Popup>Preview Card</PreviewCard.Popup>
  </PreviewCard.Positioner>
</PreviewCard.Root>

You can also change the alignment of the Preview Card in relation to its anchor. By default, it is centered, but it can be aligned to an edge of the anchor using the alignment prop:

<PreviewCard.Positioner side="right" alignment="start">
  <PreviewCard.Popup>Preview Card</PreviewCard.Popup>
</PreviewCard.Positioner>

Due to collision detection, the Preview Card may change its placement to avoid overflow. Therefore, your explicitly specified side and alignment props act as "ideal", or preferred, values.

To access the true rendered values, which may change as the result of a collision, the popup element receives data attributes:

// Rendered HTML (simplified)
<div>
  <div data-side="left" data-alignment="end">
    Preview Card
  </div>
</div>

This allows you to conditionally style the Preview Card based on its rendered side or alignment.

Offset

The sideOffset prop creates a gap between the anchor and Preview Card popup, while alignmentOffset slides the Preview Card popup from its alignment, acting logically for start and end alignments.

<PreviewCard.Positioner sideOffset={10} alignmentOffset={10}>

Delay

To change how long the Preview Card waits until it opens or closes, use the delay and closeDelay props, which represent how long the Preview Card waits after the cursor rests on the trigger to open, or moves away from the trigger to close, in milliseconds:

<PreviewCard.Root delay={200} closeDelay={200}>

The delay type can be changed from "rest" (user's cursor is static over the trigger for the given timeout in milliseconds) to "hover" (the user's cursor has entered the trigger):

<PreviewCard.Root delayType="hover">

Controlled

To control the Preview Card with external state, use the open and onOpenChange props:

function App() {
  const [open, setOpen] = React.useState(false);
  return (
    <PreviewCard.Root open={open} onOpenChange={setOpen}>
      {/* Subcomponents */}
    </PreviewCard.Root>
  );
}

Arrow

To add an arrow (caret or triangle) inside the Preview Card content that points toward the center of the anchor element, use the PreviewCard.Arrow component:

<PreviewCard.Positioner>
  <PreviewCard.Popup>
    <PreviewCard.Arrow />
    Preview Card
  </PreviewCard.Popup>
</PreviewCard.Positioner>

It automatically positions a wrapper element that can be styled or contain a custom SVG shape.

Backdrop

You may dim content behind the Preview Card in order to draw more attention to it by rendering an optional backdrop.

<PreviewCard.Root>
  <PreviewCard.Backdrop />
  {/* Subcomponents */}
</PreviewCard.Root>

It has the same maximum z-index as the Positioner component by default, and should be placed before it in the React tree.

Anchoring

By default, the Trigger acts as the anchor, but this can be changed to another element.

  • A DOM element (stored in React state):
<PreviewCard.Positioner anchor={anchorNode}>
  • A React ref:
<PreviewCard.Positioner anchor={anchorRef}>
  • A virtual element object, consisting of a getBoundingClientRect method and an optional contextElement property:
<PreviewCard.Positioner
  anchor={{
    getBoundingClientRect: () => DOMRect,
    // `contextElement` is an optional but recommended property when `getBoundingClientRect` is
    // derived from a real element, to ensure collision detection and position updates work as
    // expected in certain DOM trees.
    contextElement: domNode,
  }}
>

Styling

The PreviewCard.Positioner element receives the following CSS variables, which can be used by PreviewCard.Popup:

  • --anchor-width: Specifies the width of the anchor element. You can use this to match the width of the Preview Card with its anchor.
  • --anchor-height: Specifies the height of the anchor element. You can use this to match the height of the Preview Card with its anchor.
  • --available-width: Specifies the available width of the popup before it overflows the viewport.
  • --available-height: Specifies the available height of the popup before it overflows the viewport.
  • --transform-origin: Specifies the origin of the popup element that represents the point of the anchor element's center. When animating scale, this allows it to correctly emanate from the center of the anchor.

Large content

If your Preview Card is large enough that it cannot fit inside the viewport (especially on small or narrow screens as on mobile devices), the --available-width and --available-height properties are useful to constrain its size to prevent it from overflowing.

.PreviewCardPopup {
  max-width: var(--available-width);
  max-height: var(--available-height);
  overflow: auto;
}

The overflow: auto property will prevent the Arrow from appearing, if specified. You can instead place this on a wrapper child inside the Popup:

<PreviewCard.Popup className="PreviewCardPopup">
  <PreviewCard.Arrow />
  <div className="PreviewCardPopup-content">Large content</div>
</PreviewCard.Popup>
.PreviewCardPopup-content {
  max-width: var(--available-width);
  max-height: var(--available-height);
  overflow: auto;
}

Absolute maximums can also be specified if the Preview Card's size can be too large on wider or bigger screens:

.PreviewCardPopup-content {
  max-width: min(500px, var(--available-width));
  max-height: min(500px, var(--available-height));
  overflow: auto;
}

Animations

The Preview Card can animate when opening or closing with either:

  • CSS transitions
  • CSS animations
  • JavaScript animations

CSS transitions

Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior:

<PreviewCard.Popup className="PreviewCardPopup">Preview Card</PreviewCard.Popup>
.PreviewCardPopup {
  transform-origin: var(--transform-origin);
  transition-property: opacity, transform;
  transition-duration: 0.2s;
  /* Represents the final styles once exited */
  opacity: 0;
  transform: scale(0.9);
}

/* Represents the final styles once entered */
.PreviewCardPopup[data-state='open'] {
  opacity: 1;
  transform: scale(1);
}

/* Represents the initial styles when entering */
.PreviewCardPopup[data-entering] {
  opacity: 0;
  transform: scale(0.9);
}

Styles need to be applied in three states:

  • The exiting styles, placed on the base element class
  • The open styles, placed on the base element class with [data-state="open"]
  • The entering styles, placed on the base element class with [data-entering]

In newer browsers, there is a feature called @starting-style which allows transitions to occur on open for conditionally-mounted components:

/* Base UI API - Polyfill */
.PreviewCardPopup[data-entering] {
  opacity: 0;
  transform: scale(0.9);
}

/* Official Browser API - no Firefox support as of May 2024 */
@starting-style {
  .PreviewCardPopup[data-state='open'] {
    opacity: 0;
    transform: scale(0.9);
  }
}

CSS animations

CSS animations can also be used, requiring only two separate declarations:

@keyframes scale-in {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
}

@keyframes scale-out {
  to {
    opacity: 0;
    transform: scale(0.9);
  }
}

.PreviewCardPopup {
  animation: scale-in 0.2s forwards;
}

.PreviewCardPopup[data-exiting] {
  animation: scale-out 0.2s forwards;
}

JavaScript animations

The keepMounted prop lets an external library control the mounting, for example framer-motion's AnimatePresence component.

function App() {
  const [open, setOpen] = useState(false);
  return (
    <PreviewCard.Root open={open} onOpenChange={setOpen}>
      <PreviewCard.Trigger>Trigger</PreviewCard.Trigger>
      <AnimatePresence>
        {open && (
          <PreviewCard.Positioner keepMounted>
            <PreviewCard.Popup
              render={
                <motion.div
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                />
              }
            >
              Preview Card
            </PreviewCard.Popup>
          </PreviewCard.Positioner>
        )}
      </AnimatePresence>
    </PreviewCard.Root>
  );
}

Animation states

Four states are available as data attributes to animate the popup, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the keepMounted prop.

  • [data-state="open"] - open state is true.
  • [data-state="closed"] - open state is false. Can still be mounted to the DOM if closing.
  • [data-entering] - the popup was just inserted to the DOM. The attribute is removed 1 animation frame later. Enables "starting styles" upon insertion for conditional rendering.
  • [data-exiting] - the popup is in the process of being removed from the DOM, but is still mounted.

Overriding default components

Use the render prop to override the rendered elements with your own components.

// Element shorthand
<PreviewCard.Popup render={<MyPreviewCardPopup />} />
// Function
<PreviewCard.Popup render={(props) => <MyPreviewCardPopup {...props} />} />