Product
Product
Laptop Frame
Laptop lid with a bezel around your screen content, sitting on a simple deck with a hinge line.
Install
Usage
<Center>
<LaptopFrame>
<div style={{ width: "100%", height: "100%", background: "#fff" }} />
</LaptopFrame>
</Center>Props
| Name | Type | Default | Description |
|---|---|---|---|
delay? | number | — | Frames to wait before entering. |
duration? | number | — | Enter duration in frames. Defaults to 0.6s. |
exit? | number | boolean | — | Exit at the end of the parent `<Sequence>`. `false` keeps it on screen, a number sets the exit length in frames. |
motion? | MotionPreset | — | Override the theme's motion personality. |
width? | number | — | Screen width as a fraction of the safe area (0–1). Defaults to 0.78 in landscape, 0.94 otherwise. |
aspect? | number | 16 / 10 | Screen aspect ratio (width / height). Defaults to 16/10. |
children? | ReactNode | — | |
color? | string | — | Body color. Defaults to a shade between the theme surface and foreground. |
screenColor? | string | — | Screen fill behind the children. Defaults to the theme background. |
style? | CSSProperties | — | |
className? | string | — |
Use
- Framing a desktop app, dashboard or website screenshot in a launch video
- Two builds side by side inside `split-screen` for a before/after
Avoid
- A phone or tablet screen — use
phone-frame
Dependencies
Source
registry/items/laptop-frame.tsx
/**
* @title Laptop Frame
* @category product
* @description Laptop lid with a bezel around your screen content, sitting on a simple deck with a hinge line.
* @duration data-driven
* @use Framing a desktop app, dashboard or website screenshot in a launch video
* @use Two builds side by side inside `split-screen` for a before/after
* @avoid A phone or tablet screen — use `phone-frame`
* @tags laptop, desktop, computer, device, mockup, frame
* @example
* <Center>
* <LaptopFrame>
* <div style={{ width: "100%", height: "100%", background: "#fff" }} />
* </LaptopFrame>
* </Center>
*/
import type React from "react";
import { alpha, type MotionProps, useMotion, useTheme, useViewport } from "./core";
export type LaptopFrameProps = MotionProps & {
/** Screen width as a fraction of the safe area (0–1). Defaults to 0.78 in landscape, 0.94 otherwise. */
width?: number;
/** Screen aspect ratio (width / height). Defaults to 16/10. */
aspect?: number;
children?: React.ReactNode;
/** Body color. Defaults to a shade between the theme surface and foreground. */
color?: string;
/** Screen fill behind the children. Defaults to the theme background. */
screenColor?: string;
style?: React.CSSProperties;
className?: string;
};
export function LaptopFrame({
width: widthFraction,
aspect = 16 / 10,
children,
color,
screenColor,
style,
className,
...motion
}: LaptopFrameProps) {
const theme = useTheme();
const { u, width, height, safe, isLandscape } = useViewport();
const m = useMotion(motion);
const body = color ?? `color-mix(in srgb, ${theme.colors.foreground} 16%, ${theme.colors.surface})`;
const bezel = u(20);
const deckH = u(26);
const maxScreenW = (width - safe.x * 2) * (widthFraction ?? (isLandscape ? 0.78 : 0.94)) - bezel * 2;
const maxScreenH = height - safe.top - safe.bottom - bezel * 2 - deckH - u(16);
const screenW = Math.min(maxScreenW, maxScreenH * aspect);
const screenH = screenW / aspect;
const lidW = screenW + bezel * 2;
const lidH = screenH + bezel * 2;
const deckW = lidW * 1.06;
return (
<div
className={className}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
opacity: Math.min(1, Math.max(0, m.enter)) * (1 - m.exit),
translate: `0 ${(1 - m.enter) * u(40) - m.exit * u(24)}px`,
scale: String(0.96 + 0.04 * m.enter),
...style,
}}
>
<div
style={{
width: lidW,
height: lidH,
padding: bezel,
borderRadius: `${u(14)}px ${u(14)}px ${u(4)}px ${u(4)}px`,
background: body,
boxShadow: `inset 0 0 0 ${u(1.5)}px ${alpha("#000000", 0.3)}`,
}}
>
<div
style={{
width: screenW,
height: screenH,
borderRadius: u(4),
overflow: "hidden",
background: screenColor ?? theme.colors.background,
}}
>
{children}
</div>
</div>
<div
style={{
width: deckW,
height: deckH,
borderRadius: `0 0 ${u(10)}px ${u(10)}px`,
background: body,
boxShadow: `0 ${u(10)}px ${u(24)}px ${alpha("#000000", 0.35)}`,
display: "flex",
justifyContent: "center",
}}
>
{/* Hinge shadow line where the lid meets the deck. */}
<div
style={{
width: u(120),
height: u(4),
marginTop: u(-2),
borderRadius: u(2),
background: alpha("#000000", 0.25),
}}
/>
</div>
</div>
);
}