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…

import * as React from "react"
import { cn } from "@/lib/utils"

export interface DeviceFrameProps extends React.HTMLAttributes<HTMLDivElement> {
  /** phone = rounded handset shell with a notch; browser = desktop window chrome. */
  variant: "phone" | "browser"
  /** Browser variant only — plain address-bar text, not a real navigable URL. */
  url?: string
}

/**
 * A device-shaped shell for showing off a screenshot or a live component —
 * pure CSS, no measuring, no JS. The shell colors come from `bg-foreground`,
 * so it inverts for free between light and dark themes (near-black case on

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/device-frame.json

Prompt

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-hidden cosmetic scaffolding; children keep their own semantics (an image's alt, a live widget's roles) untouched.
  • Facade, not a live viewurl is plain display text; nothing is fetched, embedded or navigated. A real interactive preview is a different component's job (see the iframe facade in video-embed).
  • Foreground-as-shell inversion — the bezel is bg-foreground/90, so switching the app theme swaps the case color for free — no dark: 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.

On This Page