Preview Card
Popover's surface with the frost turned up, because this one lands on a sentence.
Installation
npx shadcn@latest add https://liqui.design/r/preview-card.jsonInstalling more than one? Register the namespace once in components.json and
drop the URLs:
{
"registries": {
"@liqui-design": "https://liqui.design/r/{name}.json"
}
}npx shadcn@latest add @liqui-design/preview-cardUsage
import {
PreviewCard,
PreviewCardContent,
PreviewCardDescription,
PreviewCardTitle,
PreviewCardTrigger,
} from '@/components/ui/preview-card';<PreviewCard>
<p>
… a <PreviewCardTrigger href="/docs/handbook/glass">displacement map</PreviewCardTrigger> …
</p>
<PreviewCardContent>
<PreviewCardTitle>Displacement map</PreviewCardTitle>
<PreviewCardDescription>…</PreviewCardDescription>
</PreviewCardContent>
</PreviewCard>The trigger renders an <a>. Compose your router's link onto it with render:
<PreviewCardTrigger render={<NextLink href="/docs" />}>displacement map</PreviewCardTrigger>Delay
Notes
The frost is higher than a popover's
const POPUP_GLASS = { elevated: true, radius: 18, blur: 1, refraction: 120, bezel: 24, frost: 0.6 };Every other popup in the library opens because you asked for it, and lands on layout: a menu over a page, a dialog over a scrim, a tooltip over a button. A preview card opens because your pointer drifted onto a link, and it lands on the paragraph you were reading.
At the popover's frost: 0.35 the words underneath come through the middle of
the card, and the reader loses the sentence they were in — not to a design
choice, but to a hover they did not make. So this one sits at 0.6: the bezel
still bends the edges of whatever is behind it, and the body is opaque enough to
read on.
frost is the kernel's density dial — Apple's "clear" at 0 and "regular" at 1 —
and it interpolates the tint opacity while adding up to 14px of blur on top of
blur. Turning it up is how liqui buys legibility without giving up the lens,
and this is the component that needs to buy some.
Which means the tail's numbers are not the popover's
The tail is not a LiquiGlass. It cannot be — the surface is a rounded
rectangle, and a 20×10 triangle has no room for a bezel to refract through — so
it is a clip-path wearing the popup's material by hand:
const ARROW_BACKDROP = 'blur(9.4px) saturate(1.7)'; // blur + frost × 14
const ARROW_TINT = '…52% → 70%…'; // 0.25 + 0.75 × frostThose are the kernel's own formulas evaluated at this component's frost.
Popover says the tail does not follow frost and
warns you to check it; this component is what that warning looks like when it
is taken up. Change frost through the glass prop and the two constants above
are the thing to fix.
Delay is a performance decision too
delay defaults to 600ms and closeDelay to 300ms, and the obvious reason is
manners — a card that fires on every link the pointer crosses is a page that
flinches.
The less obvious reason is that the popup's displacement map is generated the first time it opens at a given size. A card that fires while the reader is still travelling is asking for that work at the worst moment, on a pointer path that may cross four more triggers. Six hundred milliseconds means they stopped, and it means the surface is being built for someone who is going to look at it.