Laptop Frame

Wraps any other composition inside a realistic laptop mockup with a drop-in entrance.

Preview

Open editor

Usage

The laptop counterpart to PhoneFrame. Pick any other composition from the library and it renders inside a realistic laptop mockup — choose Space Gray or Silver — with a spring drop-in entrance.

The inner composition uses its own default props and is rendered into the screen area at its native dimensions, scaled to fill with cover fit. Reach for this when you want a software-product feel: wrap a BrowserWindow for a SaaS demo, a CursorWalkthrough for an onboarding clip, or a TypingSearch for a search-product hero.

The composition is 1920×1080 (16:9) — same aspect as YouTube, landing-page hero videos, and most product launch slates.

Props

NameTypeDefault
chassis"space-gray" | "silver""space-gray"
screenImagestring (url)""
innerCompositionIdstring (composition id)"BrowserWindow"
innerPropsRecord<string, unknown>

Composition

ID
LaptopFrame
Resolution
1920×1080
FPS
60
Duration
13.0s

Source

Copy or download the React source — drop it into your own Remotion project. The only runtime dependency is remotion.

"use client";
import { AbsoluteFill, Img, spring, useVideoConfig } from "remotion";
import { type ClipStyle, resolveClipStyle } from "../../clip-style";
import { componentsByIdBase as componentsById } from "../../componentsBase";
import { proxyExternalImg } from "../../proxy-image";
import { compositionsById } from "../../registry";
import { snap } from "../../snap";
import { useDesignFrame } from "../../use-design-frame";

export type LaptopFrameProps = {
  chassis: "silver" | "space-gray";
  innerCompositionId: string;
  screenImage: string;
  innerProps?: Record<string, unknown>;
  clipStyle?: ClipStyle;
};

const LID_W = 1440;
const LID_H = 900;
const BEZEL = 22;
const SCREEN_W = LID_W - BEZEL * 2;
const SCREEN_H = LID_H - BEZEL * 2;
const LID_RADIUS = 26;
const SCREEN_RADIUS = 8;
const HINGE_GAP = 4;
const BASE_LIP_W = 1560;
const BASE_LIP_H = 26;
const BASE_LIP_RADIUS = 6;
const BOTTOM_SHADOW_W = 1620;
const BOTTOM_SHADOW_H = 8;

type ChassisColors = {
  lidEdge: string;
  baseLip: string;
  baseLipShadow: string;
  bottomShadow: string;
};

function getChassis(chassis: LaptopFrameProps["chassis"]): ChassisColors {
  if (chassis === "space-gray") {
    return {
      lidEdge: "linear-gradient(180deg, #2a2a2e 0%, #1a1a1d 50%, #242427 100%)",
      baseLip: "linear-gradient(180deg, #1f1f22 0%, #2c2c30 30%, #1a1a1d 100%)",
      baseLipShadow: "0 4px 14px rgba(0,0,0,0.45)",
      bottomShadow: "rgba(0,0,0,0.55)",
    };
  }
  return {
    lidEdge: "linear-gradient(180deg, #d8d8de 0%, #c0c0c8 55%, #d4d4da 100%)",
    baseLip: "linear-gradient(180deg, #c4c4ca 0%, #d8d8de 30%, #b6b6bc 100%)",
    baseLipShadow: "0 4px 14px rgba(15,16,20,0.18)",
    bottomShadow: "rgba(15,16,20,0.25)",
  };
}

export const LaptopFrame: React.FC<LaptopFrameProps> = ({
  chassis,
  innerCompositionId,
  screenImage,
  innerProps,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const { fps } = useVideoConfig();
  const s = resolveClipStyle(clipStyle, {
    background: "#ffffff",
    color: "#0f1014",
    fontFamily:
      "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
    accent: "#0a84ff",
  });

  const drop = spring({
    frame,
    fps,
    config: { damping: 14, stiffness: 110, mass: 0.85 },
  });
  const scale = 0.9 + drop * 0.1;
  const ty = (1 - drop) * 60;

  const Component = componentsById[innerCompositionId];
  const innerInfo = compositionsById[innerCompositionId];
  const colors = getChassis(chassis);

  return (
    <AbsoluteFill
      style={{
        background: s.background,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        overflow: "hidden",
      }}
    >
      <div
        style={{
          opacity: drop,
          transform: `translate3d(0, ${snap(ty)}px, 0) scale(${scale})`,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
        }}
      >
        {/* Display / lid */}
        <div
          style={{
            width: LID_W,
            height: LID_H,
            background: colors.lidEdge,
            borderRadius: LID_RADIUS,
            padding: BEZEL,
            position: "relative",
            boxShadow:
              "0 60px 140px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06), inset 0 0 0 1px rgba(255,255,255,0.05)",
          }}
        >
          {/* Camera notch dot */}
          <div
            style={{
              position: "absolute",
              top: BEZEL / 2 - 2,
              left: "50%",
              transform: "translateX(-50%)",
              width: 8,
              height: 8,
              borderRadius: "50%",
              background: "#0a0a0c",
              boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.06)",
            }}
          />

          <div
            style={{
              width: SCREEN_W,
              height: SCREEN_H,
              background: "#000",
              borderRadius: SCREEN_RADIUS,
              overflow: "hidden",
              position: "relative",
            }}
          >
            {screenImage ? (
              <Img
                src={proxyExternalImg(screenImage)}
                crossOrigin="anonymous"
                style={{
                  width: "100%",
                  height: "100%",
                  objectFit: "cover",
                  display: "block",
                }}
              />
            ) : Component && innerInfo ? (
              <ScaledScene
                Component={Component}
                compW={innerInfo.width}
                compH={innerInfo.height}
                defaultProps={innerInfo.defaultProps}
                overrideProps={innerProps}
              />
            ) : (
              <FallbackScreen />
            )}
          </div>
        </div>

        {/* Hinge gap */}
        <div style={{ height: HINGE_GAP }} />

        {/* Base lip — slightly wider than lid */}
        <div
          style={{
            width: BASE_LIP_W,
            height: BASE_LIP_H,
            background: colors.baseLip,
            borderRadius: BASE_LIP_RADIUS,
            boxShadow: colors.baseLipShadow,
            position: "relative",
          }}
        >
          {/* Trackpad cutout indicator */}
          <div
            style={{
              position: "absolute",
              left: "50%",
              top: "50%",
              transform: "translate(-50%, -50%)",
              width: 220,
              height: 4,
              borderRadius: 2,
              background: "rgba(0,0,0,0.18)",
            }}
          />
        </div>

        {/* Bottom contact shadow */}
        <div
          style={{
            width: BOTTOM_SHADOW_W,
            height: BOTTOM_SHADOW_H,
            marginTop: 4,
            borderRadius: 999,
            background: `radial-gradient(ellipse at center, ${colors.bottomShadow} 0%, transparent 75%)`,
            filter: "blur(2px)",
          }}
        />
      </div>
    </AbsoluteFill>
  );
};

function ScaledScene({
  Component,
  compW,
  compH,
  defaultProps,
  overrideProps,
}: {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  Component: React.ComponentType<any>;
  compW: number;
  compH: number;
  defaultProps: Record<string, unknown>;
  overrideProps?: Record<string, unknown>;
}) {
  // Contain mode: fit the whole composition inside the screen with letterbox if needed.
  const fit = Math.min(SCREEN_W / compW, SCREEN_H / compH);
  const renderedW = compW * fit;
  const renderedH = compH * fit;
  const offsetX = (SCREEN_W - renderedW) / 2;
  const offsetY = (SCREEN_H - renderedH) / 2;

  const merged = overrideProps
    ? { ...defaultProps, ...overrideProps }
    : defaultProps;

  return (
    <div
      style={{
        position: "absolute",
        inset: 0,
        background: "#000",
      }}
    >
      <div
        style={{
          position: "absolute",
          left: offsetX,
          top: offsetY,
          width: compW,
          height: compH,
          transform: `scale(${fit})`,
          transformOrigin: "top left",
        }}
      >
        <Component {...merged} />
      </div>
    </div>
  );
}

function FallbackScreen() {
  return (
    <div
      style={{
        width: "100%",
        height: "100%",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        color: "rgba(255,255,255,0.4)",
        fontFamily:
          "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
        fontSize: 28,
      }}
    >
      Pick a composition
    </div>
  );
}
Save as LaptopFrame/LaptopFrame.tsx