component

Pan Zoom

A pan and zoom viewport for images, SVG, and any DOM content — shared across Astro, Svelte, and React from one framework-agnostic core. It supports mouse-drag and touch panning, pinch and wheel zooming, keyboard navigation, a live zoom readout, fit modes, fullscreen, and an optional minimap.

Overview

Drag to pan, Ctrl/⌘ + scroll (or pinch) to zoom, and double-click to reset. Focus the viewport for full keyboard control: arrow keys pan, +/- zoom, and 0 resets. The built-in controls zoom in/out, reset, and toggle fullscreen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---

<PanZoom size="md">
  <Content />
</PanZoom>

Large content

Set maxZoom to bound how far the user can zoom in. The view always opens fitted to the content with padding.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---

<!-- A larger surface that benefits from a max zoom of 3. -->
<PanZoom size="lg" maxZoom={3}>
  <Content />
</PanZoom>

SVG content

Any DOM works as content, including inline SVG. Use minZoom/maxZoom to frame the useful zoom range.

---
import PanZoom from "../PanZoom.astro";
---

<PanZoom size="md" minZoom={0.5} maxZoom={4}>
  <svg
    width="480"
    height="320"
    viewBox="0 0 480 320"
    xmlns="http://www.w3.org/2000/svg"
  >
    <rect width="480" height="320" fill="#1e293b"></rect>
    <circle cx="160" cy="160" r="90" fill="#38bdf8"></circle>
    <rect x="260" y="80" width="140" height="140" rx="16" fill="#f472b6"></rect>
    <path
      d="M40 280 L240 40 L440 280 Z"
      fill="none"
      stroke="#fde047"
      stroke-width="4"></path>
  </svg>
</PanZoom>

Fit modes

fitMode controls how the view lays content out on open and reset: contain (default, fit with padding), cover (fill the viewport), or actual-size (1:1). Enable showZoomIndicator for a live zoom-percentage readout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
100%
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---

<!-- `cover` fills the viewport instead of fitting with padding. -->
<PanZoom size="md" fitMode="cover" showZoomIndicator>
  <Content />
</PanZoom>

Minimap

showMinimap adds an overview in the corner with a draggable rectangle marking the visible region — drag it to pan large content quickly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
100%
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---

<!-- The minimap shows the visible slice and is draggable to pan. -->
<PanZoom size="lg" showMinimap showZoomIndicator>
  <Content />
</PanZoom>

Controlled zoom

The zoom level is controllable and observable. In Svelte use bind:zoom; in React pass zoom + onZoomChange; in Astro seed the initial level with the zoom prop. onPanChange and onFullscreenChange are also available.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
150%
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---

<!-- Seeded at 1.5x with a live zoom-percentage readout. -->
<PanZoom size="md" zoom={1.5} showZoomIndicator>
  <Content />
</PanZoom>

Keyboard & accessibility

The viewport is a focusable region with an accessible label. Once focused, arrow keys pan, +/- zoom, and 0 resets. Smooth zoom transitions are skipped automatically under prefers-reduced-motion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
100%
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---

<!-- Focus the viewport, then use arrows to pan, +/- to zoom, 0 to reset. -->
<PanZoom size="md" showZoomIndicator label="Keyboard-navigable diagram">
  <Content />
</PanZoom>

Custom styling

Every slot is restylable through passThrough, and behavior is tunable via wheelZoomSpeed, pinchSensitivity, constrainToBounds, and more.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---

<!-- passThrough restyles slots; wheel zoom is faster and bounds are constrained. -->
<PanZoom
  size="md"
  wheelZoomSpeed={0.15}
  maxZoom={8}
  constrainToBounds
  passThrough={{
    container: {
      style: { borderColor: "primary.border.accent", borderRadius: "lg" },
    },
    controls: { style: { bottom: "lg", right: "lg" } },
  }}
>
  <Content />
</PanZoom>
props · 49 shown · 49 total
active
boolean"current""checked""current-within"
default false

Mark this as the chosen one in a set (a current nav item, a checked option, a selected card). Where `interactive` says it *can* be clicked, `active` says it *is* the current choice. Use `activeEmphasis` to control how loud the state is. Combined with `interactive`, hover and press restart their stepping from the active baseline rather than falling back to the unchosen ramp. The three values differ only in what drives the state: `true` by the prop (via `data-panel-active`), `"checked"` by a wrapped native input (`:has(input:checked)` — zero JS, nothing to re-render), and `"current"` by an existing `aria-current`.

activeEmphasis
"primary""secondary""tertiary"
default the panel's `emphasis`

How loud the `active` state is. Defaults to the panel's own `emphasis`, so the chosen state lands on the same ramp as the resting look — this prop is the override for when the active item should be louder (or quieter) than the panel itself. `"primary"` fills with the feedback accent ramp and flips text to the contrast scale (~150 RGB units of separation — the option that reads at a glance in a dense list). `"secondary"` tints the feedback surface ramp and `"tertiary"` the neutral ramp; both promote text to `bold` and lift the border to the default line, but move the surface only ~7–18 units.

animationDuration
number
default 150

Duration of smooth-zoom transitions, in milliseconds.

as
"button""div""section""article""aside""main""header""footer""nav""dialog""form""fieldset""ul""ol""li""a""label""span""kbd"
default "div"

HTML element to render. Curated to container-like tags so semantic intent stays clear (no html/script/style/etc).

background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Surface background from the Panel scale (`surface.peak` → `surface.ground`, or `transparent`).

border
"none""bold""default""muted""accent"

Border style: `none`, `default`, `bold`, or `muted`.

borderActive
"none""bold""inherit""default""muted""accent"

Border color while the surface is `active` — the explicit form of the ring `activeEmphasis` would otherwise pick. It holds through hover, so a `borderInteract` color can't steal it. `inherit` keeps whatever `activeEmphasis` chose; `none` clears it.

borderInteract
"none""bold""default""muted""accent"

Border style applied on hover / focus interaction.

children slot svelte
Snippet

Content to pan and zoom — any DOM, image, or SVG.

constrainToBounds
boolean
default false

Constrain panning so the content edges cannot be dragged inside the viewport once the content covers it.

disabled
boolean
default false

Disable all interaction and dim the component.

doubleTapToZoom
boolean
default true

Enable double-tap-to-zoom on touch devices.

element binding svelte
HTMLElementnull

Bindable reference to the root container element. Typed `HTMLElement` because the container is a Panel root — `as` can render any container tag.

emphasis
"primary""secondary""tertiary"

Visual emphasis / prominence level.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic feedback color forwarded via `colorPalette`. Tints the container surface, focus ring, minimap rectangle, and accent surfaces.

fitMode
"contain""cover""actual-size"
default "contain"

How `reset()` and the reset button lay content out: fit-with-padding (`contain`), fill the viewport (`cover`), or 1:1 (`actual-size`).

interactive
boolean

Enable interactive (hover / focus / press) affordances and states.

label
string
default "Pan and zoom viewport"

Accessible label for the interactive viewport region.

maxZoom
number
default 5

Highest zoom level the user can reach.

minZoom
number

Lowest zoom level the user can reach. When omitted it is auto-computed to fit the content with padding (never below `0.01`).

onFullscreenChange svelte
(isFullscreen: boolean) => void

Fires when fullscreen is entered or exited.

onPanChange svelte
(translate: Point) => void

Fires whenever the pan translation changes.

onZoomChange svelte
(scale: number) => void

Fires whenever the zoom level changes (gesture, button, key, or set).

padding
number
default 16

Padding (px) kept around the content when fitting it to the viewport. This is the FIT inset, not a CSS box padding — the Panel surface underneath is always `padding: "none"` so the viewport fills the container.

panButton
012
default 0

Which mouse button starts a drag-pan: `0` left, `1` middle, `2` right. Picking `2` also suppresses the viewport's context menu.

panOnDrag
boolean
default true

Whether dragging the viewport pans the content. Turn it off when the content owns the drag gesture (a canvas whose empty space rubber-band selects, an editor with its own marquee). Individual descendants can opt out without disabling panning wholesale by carrying a `data-no-pan` attribute — a drag starting on that element, or anywhere inside it, is left alone.

passThrough
PanZoomPassThrough< ContainerEl, ViewportEl, ContentEl, ControlsEl, ZoomInButtonEl, ZoomOutButtonEl, ResetButtonEl, FullscreenButtonEl, ZoomIndicatorEl, MinimapEl >

Per-slot style and HTML-attribute override bag.

pinchSensitivity
number
default 1

Pinch-gesture sensitivity. Lower values make pinch-zoom more sensitive.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius override from the spacing scale (or `full` for fully rounded).

radiusBottom
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the bottom-left and bottom-right corners.

radiusLeft
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-left and bottom-left corners.

radiusRight
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-right and bottom-right corners.

radiusTop
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-left and top-right corners.

reactive
boolean
default false

Serialize the panel's resolved surface inputs into a `data-panel-config` attribute on the root, enabling runtime surface changes via `updatePanelElement()` (and, for inheritors like Card, `updateCard()`) without a framework runtime — the primary consumer is Astro / vanilla JS, where props don't re-render. Reactive frameworks (Svelte/React/Vue) don't need this: their prop changes re-run the connect already.

reserveBorderSpace
boolean

Keep (or drop) the always-1px transparent border the panel reserves so a border appearing or changing never shifts layout. Computed automatically — it's reserved when a resting `border` is visible, or when `interactive` / `borderInteract` / `active` can change the border at runtime. Set `false` only on a panel that must not occupy that 1px (e.g. a borderless housing frame that would otherwise add 2px around a set).

resetOnDoubleClick
boolean
default true

Double-click (or double-tap) resets the view to the fitted state.

shadow
"sm""md""lg""xl""none""xs"

Elevation shadow applied to the surface.

showControls
boolean
default true

Render the zoom in/out/reset (and optionally fullscreen) control buttons.

showFullscreenButton
boolean
default true

Render the fullscreen toggle button (within the controls).

showMinimap
boolean
default false

Render a minimap overview with a draggable viewport rectangle.

showZoomIndicator
boolean
default false

Render the live zoom-percentage readout.

size
"sm""md""lg"
default "md"

Visual size variant — sets the minimum height of the container.

smoothZoom
boolean
default true

Animate discrete zoom steps (buttons, keys, double-click). Drag and pinch are always instant. Ignored under `prefers-reduced-motion: reduce`.

translucent
boolean

Apply a frosted-glass effect with backdrop blur over a surface background.

wheelBehavior
"zoom""ctrl-zoom""pan"
default "ctrl-zoom"

What a wheel / trackpad scroll does. `"ctrl-zoom"` zooms only with Ctrl/⌘ held (leaving plain scroll to the page), `"zoom"` always zooms, and `"pan"` scrolls the content on both axes while Ctrl-scroll still zooms.

wheelZoomSpeed
number
default 0.05

Wheel / trackpad zoom sensitivity. Higher zooms faster per wheel tick.

zoom
number

Initial / controlled zoom level. Two-way bindable in Svelte (`bind:zoom`) and controllable in React (`zoom` + `onZoomChange`).

zoomOnDoubleClick
boolean
default false

Double-click (or double-tap) zooms in toward the pointer instead of resetting. Takes precedence over `resetOnDoubleClick`.

zoomSpeed
number
default 0.2

Zoom step applied by the zoom-in/zoom-out buttons, `+`/`-` keys, and double-click-to-zoom.

Plus all standard <div> HTML attributes.

Type

  • Components
  • Blocks