Zoom Pan
betaA frame for anything bigger than its card, like a bracket: drag to move, ⌘ or Ctrl and scroll to zoom, with −, +, and fit buttons.
Preview
Round 1
Round 2
Final vote
Top pick
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/zoom-pan
Use it for
- Show one big thing that will not fit its card, like a bracket or a floor plan, and let people move around it.
- Give it a height. The content keeps its own size, and the frame starts it at 100%, centered where it fits.
- Drag to move. ⌘ or Ctrl with the wheel zooms around the pointer, and so does a trackpad pinch. Two fingers zoom on a phone.
- A plain scroll still scrolls the page. A hint in the middle says how to zoom until the scrolling stops.
- −, +, and fit sit at the bottom right. Fit shows it all and never zooms past 100% to do it.
- Name it with aria-label, like "Floor plan".
Avoid it for
- A long list or a wide table. Let the page scroll, or use Table.
- Content people must read closely at every size. Zoomed out, small text is hard to read.
- A frame taller than the screen. Its drags and the page's scroll would fight.
Props
- children
What to move and zoom. It keeps its own size.
ReactNode
- height
The frame's height in pixels.
number · default 320
- min
The least zoom, as a fraction. 0.25 is 25%.
number · default 0.25
- max
The most zoom. 2 is 200%.
number · default 2
- step
How much − and + change the zoom.
number · default 0.25
- aria-label
Names the frame for screen readers.
string · default "Zoom and pan"
- className
Extra classes on the frame. Later classes win, so className='bg-card' replaces the muted fill.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | - | What to move and zoom. It keeps its own size. |
| height | number | 320 | The frame's height in pixels. |
| min | number | 0.25 | The least zoom, as a fraction. 0.25 is 25%. |
| max | number | 2 | The most zoom. 2 is 200%. |
| step | number | 0.25 | How much − and + change the zoom. |
| aria-label | string | "Zoom and pan" | Names the frame for screen readers. |
| className | string | - | Extra classes on the frame. Later classes win, so className='bg-card' replaces the muted fill. |
Accessibility
- The frame is a named group and a Tab stop. Its description says how to use the keys.
- With the frame focused, the arrow keys move, + and − zoom, and 0 fits it all in view.
- Keys pressed on controls inside the content stay with those controls.
- A zoom from a button or a key is said through a polite live message, like "Zoom 150%".
- The −, +, and fit buttons have names. − and + stay focusable at the limit, so focus does not jump away.
- A press that does not move still clicks what is under it. A drag never clicks.
- The zoom hint is hidden from screen readers. The frame's description already says how to zoom.
- With reduced motion, the buttons and keys land the view at once.
Examples
A bracket bigger than its card
<ZoomPan height={300} aria-label="Vote bracket">
<div className="flex gap-10 p-4">
<Card className="w-56 p-4">Dark mode 19 – 17 Shortcuts</Card>
<Card className="w-56 p-4">Top pick: Dark mode</Card>
</div>
</ZoomPan>Zoom in to 400%, in steps of 50%
<ZoomPan height={400} max={4} step={0.5} aria-label="Floor plan">
<img src="/floor-plan.svg" alt="Floor plan" width={1200} height={800} />
</ZoomPan>