Preview Card
Preview Cards are visual-only, interactive popups that—when hovered—display a concise preview of the content that a link navigates to.
Introduction
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 { 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}>
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 optionalcontextElement
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
:
--preview-card-anchor-width
: Specifies the width of the anchor element. You can use this to match the width of the Preview Card with its anchor.--preview-card-anchor-height
: Specifies the height of the anchor element. You can use this to match the height of the Preview Card with its anchor.--preview-card-available-width
: Specifies the available width of the popup before it overflows the viewport.--preview-card-available-height
: Specifies the available height of the popup before it overflows the viewport.--preview-card-arrow-origin
: Specifies the origin of the popup element that represents the point of the anchor element's center, where the arrow points. 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 --preview-card-available-width
and --preview-card-available-height
properties are useful to constrain its size to prevent it from overflowing.
.PreviewCardPopup {
max-width: var(--preview-card-available-width);
max-height: var(--preview-card-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(--preview-card-available-width);
max-height: var(--preview-card-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(--preview-card-available-width));
max-height: min(500px, var(--preview-card-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(--preview-card-arrow-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-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-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-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-open]
-open
state istrue
.[data-closed]
-open
state isfalse
. 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} />} />
API Reference
PreviewCardRoot
Prop | Type | Default | Description |
---|---|---|---|
animated | bool | true | Whether the preview card can animate, adding animation-related attributes and allowing for exit animations to play. Useful to disable in tests to remove async behavior. |
closeDelay | number | 300 | The delay in milliseconds until the preview card popup is closed when openOnHover is true . |
defaultOpen | bool | false | Whether the preview card popup is open by default. Use when uncontrolled. |
delay | number | 600 | The delay in milliseconds until the preview card popup is opened when openOnHover is true . |
onOpenChange | func | Callback fired when the preview card popup is requested to be opened or closed. Use when controlled. | |
open | bool | false | Whether the preview card popup is open. Use when controlled. |
PreviewCardTrigger
Prop | Type | Default | Description |
---|---|---|---|
className | union | Class names applied to the element or a function that returns them based on the component's state. | |
render | union | A function to customize rendering of the component. |
PreviewCardPositioner
Prop | Type | Default | Description |
---|---|---|---|
alignment | enum | 'center' | The alignment of the preview card element to the anchor element along its cross axis. |
alignmentOffset | number | 0 | The offset of the preview card element along its alignment axis. |
anchor | union | The anchor element to which the preview card popup will be placed at. | |
arrowPadding | number | 5 | Determines the padding between the arrow and the preview card popup's edges. Useful when the preview card popup has rounded corners via border-radius . |
className | union | Class names applied to the element or a function that returns them based on the component's state. | |
collisionBoundary | union | 'clippingAncestors' | The boundary that the preview card element should be constrained to. |
collisionPadding | union | 5 | The padding of the collision boundary. |
container | union | The container element to which the preview card popup will be appended to. | |
hideWhenDetached | bool | false | If true , the preview card will be hidden if it is detached from its anchor element due to differing clipping contexts. |
keepMounted | bool | false | If true , preview card stays mounted in the DOM when closed. |
positionMethod | enum | 'absolute' | The CSS position strategy for positioning the preview card popup element. |
render | union | A function to customize rendering of the component. | |
side | enum | 'bottom' | The side of the anchor element that the preview card element should align to. |
sideOffset | number | 0 | The gap between the anchor element and the preview card element. |
sticky | bool | false | If true , allow the preview card to remain in stuck view while the anchor element is scrolled out of view. |
PreviewCardPopup
Prop | Type | Default | Description |
---|---|---|---|
className | union | Class names applied to the element or a function that returns them based on the component's state. | |
render | union | A function to customize rendering of the component. |
PreviewCardArrow
Prop | Type | Default | Description |
---|---|---|---|
className | union | Class names applied to the element or a function that returns them based on the component's state. | |
hideWhenUncentered | bool | false | Whether the Arrow is hidden when it can't point to the center of the anchor element. |
render | union | A function to customize rendering of the component. |
PreviewCardBackdrop
Prop | Type | Default | Description |
---|---|---|---|
className | union | Class names applied to the element or a function that returns them based on the component's state. | |
container | union | The element the Backdrop is appended to. | |
keepMounted | bool | false | Whether the Backdrop remains mounted when the Preview Card Popup is closed. |
render | union | A function to customize rendering of the component. |