Media
Device Frame
A CSS-only phone or browser shell for framing a screenshot or a live component in marketing layouts.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/device-frame.jsonPrompt
Build a React + TypeScript + Tailwind "DeviceFrame" component (no extra
runtime deps beyond `cn()`).
Contract
- Exports `DeviceFrameProps extends React.HTMLAttributes<HTMLDivElement>`:
`variant: "phone" | "browser"` (required), `url?: string` (browser only,
plain display text), `children?: ReactNode`, `className?: string`. Root
forwards a ref and spreads the remaining native div props.
Behavior
- No state, no hooks, no "use client" — variant is a pure render branch.
- phone: a handset shell with a notch and a clipped screen area sized to an
iPhone-like aspect ratio; children fill the screen area.
- browser: a window card with a three-dot title bar and an address-bar
pill; the pill renders only when `url` is passed (no url → the pill is
omitted, not left empty) and it never becomes a link or an iframe — it is
decoration, not navigation.
- Every chrome element (notch, dots, address pill) is `aria-hidden`; the
screen/content area imposes no aria of its own, so children's own
semantics (an image's alt text, a live component's roles) pass through
untouched.
Rendering & styling
- Semantic tokens only: shell = `bg-foreground/90` + `border-foreground/90`
(this is what makes the shell invert between light/dark for free), window
card = `bg-card` + `border`, title bar = `bg-muted/40`, dots =
`bg-muted-foreground/30` (never literal red/yellow/green), address pill =
`bg-muted text-muted-foreground`. `cn()` merges the consumer's className.
- phone screen: `aspect-[9/19.5] rounded-[2rem] overflow-hidden`. browser
content area: `aspect-video overflow-hidden`. Both just clip — filling
the area (`object-cover`, `size-full`) is left to whatever is passed as
children.
Customization levers
- Shell tone: swap `bg-foreground/90` for a fixed token (e.g. a dark
surface token) to pin one case color instead of inverting with the theme.
- Phone proportions: change `aspect-[9/19.5]` and the corner radii
(`rounded-[2.5rem]`/`rounded-[2rem]`) for a tablet-like or older-handset
silhouette.
- Marketing pose: wrap the rendered frame in a parent with `rotate-*` and
`[perspective:1000px]` (plus a heavier shadow) for an angled product-shot
look — the frame itself stays flat and unrotated.
- Pairing: render two frames side by side (e.g. a phone + a browser) inside
a flex/grid wrapper to build a combined "on every device" hero panel.Concepts
- Decorative chrome vs. real content — the notch, dots and address pill are
aria-hiddencosmetic scaffolding; children keep their own semantics (an image'salt, a live widget's roles) untouched. - Facade, not a live view —
urlis plain display text; nothing is fetched, embedded or navigated. A real interactive preview is a different component's job (see the iframe facade invideo-embed). - Foreground-as-shell inversion — the bezel is
bg-foreground/90, so switching the app theme swaps the case color for free — nodark:variants to maintain on the shell. - Clipping, not scaling — the screen area only clips overflow; filling it (
object-cover,size-full) is the child's responsibility, which keeps the frame agnostic to whatever is dropped inside it.