Browser Frame
Desktop browser chrome that wraps whatever you put inside it — a screenshot, a video, or the live component itself. Three variants, no image asset, and the window controls are grey rather than a copy of one vendor’s.
The product-hero arrangement: a handset overlapping a laptop, overlapped with a negative inline-start margin so both stay in flow and the group keeps an honest height. Stacks below sm instead of shrinking to nothing.
components/ui/device-showcase.tsxcomponents/ui/laptop-frame.tsxcomponents/ui/phone-frame.tsxnpx hoverlab add device-showcase
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Interactive, in your current theme. Every state below is the real component — type in it, tab through it, switch the theme.
One file. Create components/ui/device-showcase.tsx and paste.
/**
* <DeviceShowcase> — a laptop with a handset overlapping it.
*
* The arrangement every product hero uses, and the one that is fiddly enough
* that people paste a screenshot of it instead. Two things make it fiddly and
* both are handled here:
*
* 1. **The overlap is a negative margin, not absolute positioning.** Absolute
* positioning takes the phone out of flow, so the container collapses to
* the laptop's height and the phone hangs over whatever is below it. A
* negative inline-start margin keeps both in flow and keeps the group's
* height honest.
*
* 2. **It stacks rather than shrinking.** Below `sm` the two frames sit one
* above the other with the overlap removed, because a laptop scaled to fit
* a 360px viewport has a screen 200px wide and shows nothing. Every
* image-based version of this arrangement simply gets smaller.
*
* The negative margin is `-ms-` and not `-ml-`, so the phone sits on the
* correct side of the laptop in an RTL layout rather than off the far edge.
*/
import * as React from 'react'
import { LaptopFrame } from './laptop-frame'
import { PhoneFrame } from './phone-frame'
export interface DeviceShowcaseProps {
/** What the laptop shows. */
desktop?: React.ReactNode
/** What the handset shows. */
mobile?: React.ReactNode
/** Which side the handset sits on. */
phoneSide?: 'start' | 'end'
/** Laptop width in px; the handset is sized from it. */
width?: number
className?: string
}
export function DeviceShowcase({
desktop,
mobile,
phoneSide = 'end',
width = 460,
className = '',
}: DeviceShowcaseProps) {
const phone = (
<PhoneFrame
width={Math.round(width * 0.3)}
buttons={false}
className={
phoneSide === 'end'
? 'z-10 -ms-10 mt-10 hidden sm:block'
: 'z-10 -me-10 mt-10 hidden sm:block'
}
>
{mobile}
</PhoneFrame>
)
return (
<div className={`flex flex-col items-center gap-6 sm:flex-row sm:items-end sm:gap-0 ${className}`}>
{phoneSide === 'start' ? phone : null}
<LaptopFrame width={width}>{desktop}</LaptopFrame>
{phoneSide === 'end' ? phone : null}
{/* The stacked fallback. Rendered only below `sm`, where the overlapped
copy above is hidden — two elements rather than one so neither
layout has to fight the other's margins. */}
<div className="sm:hidden">
<PhoneFrame width={Math.round(width * 0.42)} buttons={false}>
{mobile}
</PhoneFrame>
</div>
</div>
)
}
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
desktopWhat the laptop shows. | React.ReactNode | — |
mobileWhat the handset shows. | React.ReactNode | — |
phoneSideWhich side the handset sits on. | 'start' | 'end' | 'end' |
widthLaptop width in px; the handset is sized from it. | number | 460 |
className | string | '' |
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Desktop browser chrome that wraps whatever you put inside it — a screenshot, a video, or the live component itself. Three variants, no image asset, and the window controls are grey rather than a copy of one vendor’s.
One handset bezel with the cut-out as a prop, so iPhone, Android and tablet are three settings rather than three assets. Status bar and home indicator included, and the clock is a prop so it can never be a hydration mismatch.
A lid, a hinge and a tapered base, with the taper done by clip-path rather than the border trick that cannot round or carry a background. Degrades to a boxier laptop where clip-path is missing.