reelcn
GitHubBack to landing

Browser window

Neutral browser chrome with back/forward arrows and a rounded url pill, wrapped around any page content you pass as children.

BrowserWindow, dashboard
BrowserWindow, dashboard thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/browser-window.json

Files land in src/reelcn/ with the items it builds on.

Usage

<Center>
  <BrowserWindow url="reelcn.dev/pricing">
    <div style={{ width: "100%", height: "100%", background: "#fff" }} />
  </BrowserWindow>
</Center>

Props

PropTypeDescription
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse duration for an unrelated local timing. See that item's own prop docs.
exitnumber | booleanExit at the end of the parent `<Sequence>`. false keeps it on screen, a number sets the exit length in frames.
motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to false.
holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to 0.
urlstringText shown in the address pill. Default "example.com".
securebooleanShows a small lock icon before the url. Default true.
widthnumberWindow width as a fraction of the safe area (0–1). Defaults to 0.86 in landscape, 1 otherwise.
aspectnumberWindow aspect ratio (width / height). Defaults to 16/10. Default 16 / 10.
childrenReactNode
barColorstringChrome bar fill. Defaults to the theme surface.
backgroundstringPage fill behind the children. Defaults to the theme background.
borderColorstring
urlColorstringAddress pill text color. Defaults to the theme muted color.
radiusnumberCorner radius in design units. Defaults to the theme radius.
styleCSSProperties
classNamestring

Built from

Use

  • Framing a website, landing page or web app screenshot in a launch or demo video
  • Two pages side by side inside `split-screen` for a before/after

Avoid

  • A native desktop app with a title bar and no address bar. Use app-window

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/browser-window.tsx
/**
 * @title Browser Window
 * @category product
 * @description Neutral browser chrome with back/forward arrows and a rounded url pill, wrapped around any page content you pass as children.
 * @duration data-driven
 * @use Framing a website, landing page or web app screenshot in a launch or demo video
 * @use Two pages side by side inside `split-screen` for a before/after
 * @avoid A native desktop app with a title bar and no address bar. Use `app-window`
 * @tags browser, chrome, url, website, mockup, frame
 * @example
 * <Center>
 *   <BrowserWindow url="reelcn.dev/pricing">
 *     <div style={{ width: "100%", height: "100%", background: "#fff" }} />
 *   </BrowserWindow>
 * </Center>
 */
import type React from "react";
import { alpha, type MotionProps, useMotion, useRoleMotion, useTheme, useViewport } from "./core";

export type BrowserWindowProps = MotionProps & {
  /** Text shown in the address pill. */
  url?: string;
  /** Shows a small lock icon before the url. */
  secure?: boolean;
  /** Window width as a fraction of the safe area (0–1). Defaults to 0.86 in landscape, 1 otherwise. */
  width?: number;
  /** Window aspect ratio (width / height). Defaults to 16/10. */
  aspect?: number;
  children?: React.ReactNode;
  /** Chrome bar fill. Defaults to the theme surface. */
  barColor?: string;
  /** Page fill behind the children. Defaults to the theme background. */
  background?: string;
  borderColor?: string;
  /** Address pill text color. Defaults to the theme muted color. */
  urlColor?: string;
  /** Corner radius in design units. Defaults to the theme radius. */
  radius?: number;
  style?: React.CSSProperties;
  className?: string;
};

export function BrowserWindow({
  url = "example.com",
  secure = true,
  width: widthFraction,
  aspect = 16 / 10,
  children,
  barColor,
  background,
  borderColor,
  urlColor,
  radius,
  style,
  className,
  ...motion
}: BrowserWindowProps) {
  const theme = useTheme();
  const { u, width, height, safe, isLandscape } = useViewport();
  const m = useMotion(motion);
  // opacityLeadFrames: 0 keeps this item's default render pixel-identical to its pre-retrofit hardcoded
  // exit (opacity and geometry finishing together); useRoleMotion's own default (3) is unchanged for other adopters.
  const role = useRoleMotion(m, { exit: { opacityLeadFrames: 0 } });
  const border = borderColor ?? theme.colors.border;
  const bar = barColor ?? theme.colors.surface;
  const maxW = (width - safe.x * 2) * (widthFraction ?? (isLandscape ? 0.86 : 1));
  const maxH = height - safe.top - safe.bottom;
  const barH = u(58);
  const cardW = Math.min(maxW, (maxH - barH) * aspect);
  const cardH = cardW / aspect + barH;
  const r = u(radius ?? theme.radius);

  return (
    <div
      className={className}
      style={{
        width: cardW,
        height: cardH,
        borderRadius: r,
        overflow: "hidden",
        background: background ?? theme.colors.background,
        border: `1px solid ${border}`,
        boxShadow: `0 ${u(2)}px ${u(6)}px ${alpha("#000000", 0.12)}, 0 ${u(28)}px ${u(72)}px ${alpha("#000000", 0.3)}`,
        fontFamily: theme.fonts.body,
        opacity: role.opacity,
        translate: `0 ${(1 - m.enter) * u(40) - m.exit * role.travel(u(40)).exitPx}px`,
        scale: String(0.97 + 0.03 * m.enter),
        ...style,
      }}
    >
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: u(18),
          height: barH,
          padding: `0 ${u(20)}px`,
          background: bar,
          borderBottom: `1px solid ${border}`,
        }}
      >
        <div style={{ display: "flex", gap: u(9) }}>
          {[0, 1, 2].map((dot) => (
            <div
              key={dot}
              style={{ width: u(12), height: u(12), borderRadius: "50%", background: alpha(theme.colors.muted, 0.4) }}
            />
          ))}
        </div>
        <div style={{ display: "flex", gap: u(6), color: alpha(theme.colors.muted, 0.6) }}>
          <svg
            aria-hidden="true"
            viewBox="0 0 24 24"
            width={u(16)}
            height={u(16)}
            fill="none"
            stroke="currentColor"
            strokeWidth={2.4}
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <path d="M15 5l-7 7 7 7" />
          </svg>
          <svg
            aria-hidden="true"
            viewBox="0 0 24 24"
            width={u(16)}
            height={u(16)}
            fill="none"
            stroke="currentColor"
            strokeWidth={2.4}
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <path d="M9 5l7 7-7 7" />
          </svg>
        </div>
        <div
          style={{
            flex: 1,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            gap: u(8),
            height: u(36),
            borderRadius: u(18),
            background: alpha(theme.colors.muted, 0.14),
            fontSize: u(19),
            color: urlColor ?? theme.colors.muted,
          }}
        >
          {secure && (
            <svg
              aria-hidden="true"
              viewBox="0 0 24 24"
              width={u(14)}
              height={u(14)}
              fill="none"
              stroke="currentColor"
              strokeWidth={2}
            >
              <rect x="5" y="11" width="14" height="9" rx="2" />
              <path d="M8 11V7a4 4 0 0 1 8 0v4" />
            </svg>
          )}
          {url}
        </div>
      </div>
      <div style={{ position: "relative", width: cardW, height: cardH - barH, overflow: "hidden" }}>{children}</div>
    </div>
  );
}

On this page