Device frame
FreeShow live UI inside a laptop or a phone. The lid opens and the whole device turns, but the screen is an ordinary subtree laid out at the device's true width (1280 or 390 pixels) inside a size container, so breakpoints and container queries behave exactly as in the product.
pnpm dlx shadcn@latest add @caliper/device-frame- Acme Co · Team$199
- Northwind · Pro$99
- Initech · Pro$99
- Globex · Team$199
When to use #
Use it for
- Docs and pages that preview a responsive screen.
- Comparing the same screen on a laptop and a phone.
Skip it when
- A hero image. Use real UI so the preview stays honest.
- Screens with state you cannot lose: changing device remounts the content.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- True width, then scaled
- The content is laid out at 1280 or 390 CSS pixels and scaled down to fit the model, so it breaks where the product breaks.
Usage
- The body is decoration
- The laptop and phone are hidden from assistive technology. The screen content stays in the reading and tab order.
Installation #
pnpm dlx shadcn@latest add @caliper/device-frame
Adds its 7 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { DeviceFrame } from "@/components/ui/device-frame" function OverviewScreen() { return ( <div className="grid h-full content-start gap-4 p-6 text-[15px]"> <div className="flex items-baseline justify-between text-xl font-bold tracking-tight">Overview<span className="font-mono text-xs font-medium text-muted-foreground">Oct 2026</span></div> <div className="grid grid-cols-1 gap-3 @xl:grid-cols-3"> {[["MRR", "$48.2k"], ["Trials", "312"], ["Churn", "1.4%"]].map(([name, value]) => ( <div key={name} className="rounded-xl border border-border p-3"><small className="block font-mono text-xs text-muted-foreground">{name}</small><strong className="text-2xl tracking-tight tabular-nums">{value}</strong></div> ))} </div> <div className="flex h-28 items-end gap-2 border-b border-border-strong"> {[38, 52, 46, 64, 58, 82, 74].map((h, i) => <i key={i} className={`flex-1 rounded-t ${i === 5 ? "bg-brand" : "bg-foreground"}`} style={{ height: `${h}%` }} />)} </div> <ul className="m-0 grid list-none p-0"> {[["Acme Co · Team", "$199"], ["Northwind · Pro", "$99"], ["Initech · Pro", "$99"], ["Globex · Team", "$199"]].map(([name, price]) => ( <li key={name} className="flex justify-between border-b border-border py-2.5"><span>{name}</span><b>{price}</b></li> ))} </ul> </div> ); } export function DeviceFrameDemo() { return <div className="w-full max-w-3xl"><DeviceFrame><OverviewScreen /></DeviceFrame></div>; }
API reference #
DeviceFrame
component
Accessibility #
It is built on native elements and keeps aria-hidden, aria-pressed, aria-label and aria-valuetext in step with state.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.