Product
Product
Screen Zoom
Zooms and pans across whatever you wrap so a rectangle of the content — a button, a chart, a card — fills the frame, eased between keyframes.
Install
Usage
<BrowserWindow url="app.dev">
<ScreenZoom
focus={[
{ frame: 0, x: 0, y: 0, width: 100, height: 100 },
{ frame: 40, x: 55, y: 30, width: 30, height: 24 },
]}
>
<Dashboard />
</ScreenZoom>
</BrowserWindow>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 | false | 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. |
focus | ScreenZoomFocus[] | — | |
children? | ReactNode | — | |
style? | CSSProperties | — | |
className? | string | — |
Use
- Pushing in on a detail inside a `browser-window`, `app-window` or dashboard mockup
- A guided tour that visits several parts of the same screen in turn
Avoid
- Panning and zooming over a photo or footage with pixel offsets — use
camera
Dependencies
remotion
Source
registry/items/screen-zoom.tsx
/**
* @title Screen Zoom
* @category product
* @description Zooms and pans across whatever you wrap so a rectangle of the content — a button, a chart, a card — fills the frame, eased between keyframes.
* @duration data-driven
* @use Pushing in on a detail inside a `browser-window`, `app-window` or dashboard mockup
* @use A guided tour that visits several parts of the same screen in turn
* @avoid Panning and zooming over a photo or footage with pixel offsets — use `camera`
* @tags zoom, pan, focus, screen, product, tour
* @example
* <BrowserWindow url="app.dev">
* <ScreenZoom
* focus={[
* { frame: 0, x: 0, y: 0, width: 100, height: 100 },
* { frame: 40, x: 55, y: 30, width: 30, height: 24 },
* ]}
* >
* <Dashboard />
* </ScreenZoom>
* </BrowserWindow>
*/
import type React from "react";
import { AbsoluteFill } from "remotion";
import { type MotionProps, tween, useMotion } from "./core";
export type ScreenZoomFocus = {
/** Frame the frame finishes arriving here, counted from `delay`. */
frame: number;
/** Rectangle to fill the canvas with, in % of the content (0–100 on each axis). */
x: number;
y: number;
width: number;
height: number;
};
/**
* Timing comes from `focus`. Of the motion props, `delay` shifts every keyframe and `motion` picks the easing
* between them; `exit` is off by default so the zoom holds its last keyframe.
*/
export type ScreenZoomProps = MotionProps & {
focus: ScreenZoomFocus[];
children?: React.ReactNode;
style?: React.CSSProperties;
className?: string;
};
const REST: ScreenZoomFocus = { frame: 0, x: 0, y: 0, width: 100, height: 100 };
export function ScreenZoom({ focus, exit = false, children, style, className, ...motion }: ScreenZoomProps) {
const m = useMotion({ ...motion, exit });
const points = focus.slice().sort((a, b) => a.frame - b.frame);
let rect: ScreenZoomFocus = points[0] ?? REST;
for (let i = 1; i < points.length; i++) {
const from = points[i - 1];
const to = points[i];
const p = tween(m.frame, m.fps, {
from: m.delay + from.frame,
duration: Math.max(to.frame - from.frame, 1),
motion: m.preset,
});
if (p <= 0) break;
rect = {
frame: to.frame,
x: from.x + (to.x - from.x) * p,
y: from.y + (to.y - from.y) * p,
width: from.width + (to.width - from.width) * p,
height: from.height + (to.height - from.height) * p,
};
}
// Percentage translate is resolved against this element's own box, so the math holds whether ScreenZoom
// fills the whole canvas or sits nested inside a smaller frame like `browser-window`.
const scale = Math.min(100 / Math.max(rect.width, 1), 100 / Math.max(rect.height, 1));
const cx = rect.x + rect.width / 2;
const cy = rect.y + rect.height / 2;
return (
<AbsoluteFill className={className} style={{ overflow: "hidden", opacity: 1 - m.exit, ...style }}>
<AbsoluteFill
style={{ transformOrigin: "50% 50%", transform: `scale(${scale}) translate(${50 - cx}%, ${50 - cy}%)` }}
>
{children}
</AbsoluteFill>
</AbsoluteFill>
);
}