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.
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.
components/ui/laptop-frame.tsxnpx hoverlab add laptop-frame
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/laptop-frame.tsx and paste.
/**
* <LaptopFrame> — a lid, a hinge and a base around your own content.
*
* The one frame people reach for in a hero, and the one that is almost
* always a PNG. As markup it costs three divs, inherits dark mode, and lets
* the screen hold a live interface rather than a picture of one.
*
* The base is the part worth getting right and the part every CSS version
* gets wrong. A real laptop's base is wider than its lid and tapers — so it
* is drawn as a trapezoid, with the taper done by `clip-path` rather than by
* the old border trick. Borders cannot be rounded independently and cannot
* carry a background, which is why the border version always ends up as a
* flat grey wedge with a hard edge.
*
* `clip-path` degrades safely: with no support the base renders as a plain
* rectangle, which is a slightly boxier laptop and nothing worse.
*/
import * as React from 'react'
export interface LaptopFrameProps {
children?: React.ReactNode
/** Rendered width of the lid. The base extends past it on both sides. */
width?: number
/** Aspect ratio of the screen, as a Tailwind class. */
aspect?: string
/** Draw the base and hinge. Off gives a bare monitor. */
base?: boolean
className?: string
}
export function LaptopFrame({
children,
width = 420,
aspect = 'aspect-[16/10]',
base = true,
className = '',
}: LaptopFrameProps) {
return (
<div className={`shrink-0 ${className}`} style={{ width }}>
<div className="overflow-hidden rounded-xl border-[10px] border-b-[14px] border-foreground/85 bg-foreground/85 shadow-xl">
<div className={`${aspect} relative w-full overflow-hidden bg-background`}>
<span
aria-hidden
className="absolute left-1/2 top-1 z-10 size-1 -translate-x-1/2 rounded-full bg-foreground/30 border border-transparent"
/>
{children}
</div>
</div>
{base ? (
<div aria-hidden className="relative mx-auto" style={{ width: '112%' }}>
<div
className="h-3 rounded-b-lg bg-foreground/85 border border-transparent"
// 2.6% on each side is the taper. Expressed as a percentage so it
// holds at any `width`, which a pixel value would not.
style={{ clipPath: 'polygon(2.6% 0, 97.4% 0, 94% 100%, 6% 100%)' }}
/>
{/* The notch a thumb goes into. Drawn as a light bar on the base
rather than as a cut-out, because a cut-out needs a second
clip-path and shows the page through the laptop. */}
<div className="absolute left-1/2 top-0 h-1 w-16 -translate-x-1/2 rounded-b-md bg-background/25 border border-transparent" />
</div>
) : null}
</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 |
|---|---|---|
children | React.ReactNode | — |
widthRendered width of the lid. The base extends past it on both sides. | number | 420 |
aspectAspect ratio of the screen, as a Tailwind class. | string | 'aspect-[16/10]' |
baseDraw the base and hinge. Off gives a bare monitor. | boolean | true |
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.
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.