Skip to content
txbrendan/ui

Zoom Pan

beta

A 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

Mon · Design

Dark mode24
Audit log7

Mon · Engineering

Saved views15
Bulk edit12

Tue · Support

Export to CSV10
Search filters22

Tue · Sales

Offline mode5
Shortcuts19

Round 2

Wed · Team sync

Dark mode21
Saved views14

Wed · Team sync

Search filters17
Shortcuts20

Final vote

Fri · All-hands

Dark mode19
Shortcuts17

Top pick

Dark mode
Drag or use the arrow keys to move. Plus and minus zoom, and 0 fits it all in view.

Fri · All-hands

Dark mode19
Shortcuts17
Drag or use the arrow keys to move. Plus and minus zoom, and 0 fits it all in view.

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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>