reelcn
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

NameTypeDefaultDescription
delay?numberFrames to wait before entering.
duration?numberEnter duration in frames. Defaults to 0.6s.
exit?number | booleanExit at the end of the parent `<Sequence>`. `false` keeps it on screen, a number sets the exit length in frames.
motion?MotionPresetOverride the theme's motion personality.
width?numberScreen width as a fraction of the safe area (0–1). Defaults to 0.78 in landscape, 0.94 otherwise.
aspect?number16 / 10Screen aspect ratio (width / height). Defaults to 16/10.
children?ReactNode
color?stringBody color. Defaults to a shade between the theme surface and foreground.
screenColor?stringScreen 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

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>
  );
}

Related