Preview Card
A link preview opened on hover.
Anatomy
<script setup>
import { PreviewCard } from '@shardsui/vue/preview-card'
</script>
<template>
<PreviewCard.Root>
<PreviewCard.Trigger />
<PreviewCard.Portal>
<PreviewCard.Backdrop />
<PreviewCard.Positioner>
<PreviewCard.Popup>
<PreviewCard.Arrow />
<PreviewCard.Viewport />
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
</template>Usage guidelines
- Popup content should reflect the link destination: avoid placing unique or essential information in the popup unless it is also available on the linked page. Preview cards only help pointer and keyboard users; they are not accessible to touch or screen reader users.
Examples
Detached triggers
The trigger normally nests inside <PreviewCard.Root>. When the link and its card can't sit together in the markup — a link running inline in a paragraph, say — render <PreviewCard.Trigger> wherever the link belongs and tie it back to the root with a shared handle from PreviewCard.createHandle().
The handle's imperative methods, open() and close(), need a <PreviewCard.Root> using the same handle to be mounted. Calls made before a root mounts or after it unmounts are ignored, not queued. Each root starts from fresh state when it mounts.
<script setup>
const demoPreviewCard = PreviewCard.createHandle()
</script>
<template>
<!-- [!code word::handle="demoPreviewCard"] -->
<PreviewCard.Trigger :handle="demoPreviewCard" href="#">Link</PreviewCard.Trigger>
<PreviewCard.Root :handle="demoPreviewCard">...</PreviewCard.Root>
</template>Multiple triggers
One preview card can serve many links: nest several <PreviewCard.Trigger> elements in a single <PreviewCard.Root>, or point any number of detached triggers at the same handle.
<template>
<PreviewCard.Root>
<PreviewCard.Trigger href="#">Trigger 1</PreviewCard.Trigger>
<PreviewCard.Trigger href="#">Trigger 2</PreviewCard.Trigger>
...
</PreviewCard.Root>
</template><script setup>
const demoPreviewCard = PreviewCard.createHandle()
</script>
<template>
<PreviewCard.Trigger :handle="demoPreviewCard" href="#">Trigger 1</PreviewCard.Trigger>
<PreviewCard.Trigger :handle="demoPreviewCard" href="#">Trigger 2</PreviewCard.Trigger>
<PreviewCard.Root :handle="demoPreviewCard">...</PreviewCard.Root>
</template>Each trigger can feed the card its own data through the payload prop, so one card shows a different preview per link. Read it from the default slot on <PreviewCard.Root>. Pass a type argument to PreviewCard.createHandle() to type the payload:
<script setup lang="ts">
const demoPreviewCard = PreviewCard.createHandle<{ title: string }>()
</script>
<template>
<PreviewCard.Trigger :handle="demoPreviewCard" :payload="{ title: 'Trigger 1' }" href="#">
Trigger 1
</PreviewCard.Trigger>
<PreviewCard.Trigger :handle="demoPreviewCard" :payload="{ title: 'Trigger 2' }" href="#">
Trigger 2
</PreviewCard.Trigger>
<PreviewCard.Root v-slot="{ payload }" :handle="demoPreviewCard">
<PreviewCard.Portal>
<PreviewCard.Positioner :side-offset="8">
<PreviewCard.Popup>
<span v-if="payload !== undefined">Preview card opened by {{ payload.title }}</span>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
</template>Controlled mode with multiple triggers
Own the open state with v-model:open on <PreviewCard.Root>. With more than one trigger, give each trigger an id and add v-model:trigger-id to <PreviewCard.Root>: each trigger publishes its own id when it opens the card, and setting triggerId yourself anchors the card to that trigger. Pass trigger-id one-way instead if you want to drive it entirely from your own state.
Animating the Preview Card
When a single card hops between triggers, it can slide across rather than pop in and out. Position, size, and contents each animate on their own.
Position and Size
The Positioner carries the card's position: transition its left, right, top, and bottom. The Popup carries its size, so transition width and height there.
Content
The contents can cross-fade too when triggers show different previews. Wrap them in <PreviewCard.Viewport>, which detects the trigger change and sets a data-activation-direction attribute marking where the new trigger sits relative to the last — a horizontal and a vertical token separated by a space, e.g. right down; either can be empty. Match a single token with the ~= attribute selector, such as [data-activation-direction~='right'].
While a transition runs, the viewport holds both the old and new contents, each in its own wrapper:
data-current: the incoming content, or the only content when nothing is transitioning.data-previous: the outgoing content during a transition.
API reference
Root
Groups all parts of the preview card. Doesn't render its own HTML element.
Trigger
A link that opens the preview card.
Renders an <a> element.
Backdrop
An overlay displayed beneath the popup. It never receives pointer events, so hovering the page through it still works.
Renders a <div> element.
Portal
A portal that moves the popup out to <body>, clear of ancestor clipping and stacking.
Renders a <div> element.
Positioner
Positions the popup against the trigger.
Renders a <div> element.
Popup
A container for the preview card contents.
Renders a <div> element.
Viewport
A viewport for displaying content transitions.
This component is only required if one popup can be opened by multiple triggers, its content
changes based on the trigger, and switching between them is animated.
Renders a <div> element.
Set width: var(--positioner-width) and height: var(--positioner-height) on the Positioner so its box is frozen to the measured size during the transition; otherwise content-driven resizing can make the popup thrash or flip to another side.
Arrow
Displays an element positioned against the anchor.
Renders a <div> element.
Handle
Connects a <PreviewCard.Root> with detached <PreviewCard.Trigger> components, and controls the preview card imperatively. Pass a type argument to type the payload.
const previewCard = PreviewCard.createHandle<Payload>()