App window
Native desktop window with a title bar and no address bar, wrapped around any app content you pass as children.
AppWindow, overview
Installation
npx shadcn add https://www.reelcn.dev/r/app-window.jsonFiles land in src/reelcn/ with the items it builds on.
Usage
<Center>
<AppWindow title="Settings">
<div style={{ width: "100%", height: "100%", background: "#fff" }} />
</AppWindow>
</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.titlestringShown centered in the title bar.
widthnumberWindow width as a fraction of the safe area (0–1). Defaults to 0.8 in landscape, 1 otherwise.
aspectnumberWindow aspect ratio (width / height). Defaults to 16/10. Default
16 / 10.childrenReactNode
barColorstring
backgroundstring
borderColorstring
titleColorstring
radiusnumberCorner radius in design units. Defaults to the theme radius.
styleCSSProperties
classNamestring
Built from
Use
- Framing a desktop app, editor or settings panel in a launch video
- Two app windows side by side inside `split-screen`
Avoid
- A website with an address bar. Use
browser-window
Guides
Components take their colors from a theme, their timing from motion props and their layout from the canvas.
Source
registry/items/app-window.tsx
/**
* @title App Window
* @category product
* @description Native desktop window with a title bar and no address bar, wrapped around any app content you pass as children.
* @duration data-driven
* @use Framing a desktop app, editor or settings panel in a launch video
* @use Two app windows side by side inside `split-screen`
* @avoid A website with an address bar. Use `browser-window`
* @tags window, app, desktop, chrome, mockup, frame
* @example
* <Center>
* <AppWindow title="Settings">
* <div style={{ width: "100%", height: "100%", background: "#fff" }} />
* </AppWindow>
* </Center>
*/
import type React from "react";
import { alpha, type MotionProps, useMotion, useRoleMotion, useTheme, useViewport } from "./core";
export type AppWindowProps = MotionProps & {
/** Shown centered in the title bar. */
title?: string;
/** Window width as a fraction of the safe area (0–1). Defaults to 0.8 in landscape, 1 otherwise. */
width?: number;
/** Window aspect ratio (width / height). Defaults to 16/10. */
aspect?: number;
children?: React.ReactNode;
barColor?: string;
background?: string;
borderColor?: string;
titleColor?: string;
/** Corner radius in design units. Defaults to the theme radius. */
radius?: number;
style?: React.CSSProperties;
className?: string;
};
export function AppWindow({
title,
width: widthFraction,
aspect = 16 / 10,
children,
barColor,
background,
borderColor,
titleColor,
radius,
style,
className,
...motion
}: AppWindowProps) {
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.8 : 1));
const maxH = height - safe.top - safe.bottom;
const barH = u(54);
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(theme.colors.shadow, 0.12)}, 0 ${u(28)}px ${u(72)}px ${alpha(theme.colors.shadow, 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={{
position: "relative",
display: "flex",
alignItems: "center",
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>
{title && (
<div
style={{
position: "absolute",
left: "50%",
translate: "-50% 0",
fontSize: u(21),
fontWeight: 600,
color: titleColor ?? theme.colors.foreground,
}}
>
{title}
</div>
)}
</div>
<div style={{ position: "relative", width: cardW, height: cardH - barH, overflow: "hidden" }}>{children}</div>
</div>
);
}