Preview Card
A link preview opened on hover.
Anatomy
<script>
import { PreviewCard } from '@shardsui/svelte/preview-card'
</script>
<PreviewCard.Root>
<PreviewCard.Trigger />
<PreviewCard.Portal>
<PreviewCard.Backdrop />
<PreviewCard.Positioner>
<PreviewCard.Popup>
<PreviewCard.Arrow />
<PreviewCard.Viewport />
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>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>, next to the content it reveals; that's the layout in the hero demo above.
Sometimes the link and its card can't sit together in the markup, e.g. the link runs inline in a paragraph while the card is defined elsewhere. Render <PreviewCard.Trigger> wherever the link belongs and tie it back to the root with a shared handle from new PreviewCard.Handle().
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 — and each root starts from fresh state when it mounts, so no open state carries over from a previous one.
<script>
const demoPreviewCard = new PreviewCard.Handle()
</script>
<PreviewCard.Trigger handle={demoPreviewCard} href="#">Link</PreviewCard.Trigger>
<PreviewCard.Root handle={demoPreviewCard}>...</PreviewCard.Root>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.
<PreviewCard.Root>
<PreviewCard.Trigger href="#">Trigger 1</PreviewCard.Trigger>
<PreviewCard.Trigger href="#">Trigger 2</PreviewCard.Trigger>
...
</PreviewCard.Root><script>
const demoPreviewCard = new PreviewCard.Handle()
</script>
<PreviewCard.Trigger handle={demoPreviewCard} href="#">Trigger 1</PreviewCard.Trigger>
<PreviewCard.Trigger handle={demoPreviewCard} href="#">Trigger 2</PreviewCard.Trigger>
<PreviewCard.Root handle={demoPreviewCard}>...</PreviewCard.Root>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 children snippet on <PreviewCard.Root>. Pass a type argument to new PreviewCard.Handle() to type the payload:
<script>
const demoPreviewCard = new PreviewCard.Handle<{ title: string }>()
</script>
<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 handle={demoPreviewCard}>
{#snippet children({ payload })}
<PreviewCard.Portal>
<PreviewCard.Positioner sideOffset={8}>
<PreviewCard.Popup>
{#if payload !== undefined}
<span>Preview card opened by {payload.title}</span>
{/if}
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
{/snippet}
</PreviewCard.Root>Controlled mode with multiple triggers
Own the open state by binding open and handling onOpenChange on <PreviewCard.Root>. With more than one trigger, give each trigger an id and add bind:triggerId 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 triggerId 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 — 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.
Style these to author the enter and exit animations.
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.
Reach for the Viewport only when a single popup is opened by multiple triggers, its content differs per trigger, and the switch between them is animated. When you do, 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 = new PreviewCard.Handle<Payload>()