component
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.
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.
---
import PanZoom from "../PanZoom.astro";
import Content from "./_content.astro";
---
<PanZoom size="md">
<Content />
</PanZoom>Set maxZoom to bound how far the user can zoom in. The view always opens fitted to the content with padding.
---
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>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>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.
---
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>showMinimap adds an overview in the corner with a draggable rectangle marking the visible region — drag it to pan large content quickly.
---
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>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.
---
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>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.
---
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>Every slot is restylable through passThrough, and behavior is tunable via wheelZoomSpeed, pinchSensitivity, constrainToBounds, and more.
---
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>falseMark 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`.
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.
150Duration of smooth-zoom transitions, in milliseconds.
"div"HTML element to render. Curated to container-like tags so semantic intent stays clear (no html/script/style/etc).
Surface background from the Panel scale (`surface.peak` → `surface.ground`, or `transparent`).
Border style: `none`, `default`, `bold`, or `muted`.
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.
Border style applied on hover / focus interaction.
Content to pan and zoom — any DOM, image, or SVG.
falseConstrain panning so the content edges cannot be dragged inside the viewport once the content covers it.
falseDisable all interaction and dim the component.
trueEnable double-tap-to-zoom on touch devices.
Bindable reference to the root container element. Typed `HTMLElement` because the container is a Panel root — `as` can render any container tag.
Visual emphasis / prominence level.
"neutral"Semantic feedback color forwarded via `colorPalette`. Tints the container surface, focus ring, minimap rectangle, and accent surfaces.
"contain"How `reset()` and the reset button lay content out: fit-with-padding (`contain`), fill the viewport (`cover`), or 1:1 (`actual-size`).
Enable interactive (hover / focus / press) affordances and states.
"Pan and zoom viewport"Accessible label for the interactive viewport region.
5Highest zoom level the user can reach.
Lowest zoom level the user can reach. When omitted it is auto-computed to fit the content with padding (never below `0.01`).
Fires when fullscreen is entered or exited.
Fires whenever the pan translation changes.
Fires whenever the zoom level changes (gesture, button, key, or set).
16Padding (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.
0Which mouse button starts a drag-pan: `0` left, `1` middle, `2` right. Picking `2` also suppresses the viewport's context menu.
trueWhether 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.
Per-slot style and HTML-attribute override bag.
1Pinch-gesture sensitivity. Lower values make pinch-zoom more sensitive.
Corner radius override from the spacing scale (or `full` for fully rounded).
Corner radius for the bottom-left and bottom-right corners.
Corner radius for the top-left and bottom-left corners.
Corner radius for the top-right and bottom-right corners.
Corner radius for the top-left and top-right corners.
falseSerialize 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.
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).
trueDouble-click (or double-tap) resets the view to the fitted state.
Elevation shadow applied to the surface.
trueRender the zoom in/out/reset (and optionally fullscreen) control buttons.
trueRender the fullscreen toggle button (within the controls).
falseRender a minimap overview with a draggable viewport rectangle.
falseRender the live zoom-percentage readout.
"md"Visual size variant — sets the minimum height of the container.
trueAnimate discrete zoom steps (buttons, keys, double-click). Drag and pinch are always instant. Ignored under `prefers-reduced-motion: reduce`.
Apply a frosted-glass effect with backdrop blur over a surface background.
"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.
0.05Wheel / trackpad zoom sensitivity. Higher zooms faster per wheel tick.
Initial / controlled zoom level. Two-way bindable in Svelte (`bind:zoom`) and controllable in React (`zoom` + `onZoomChange`).
falseDouble-click (or double-tap) zooms in toward the pointer instead of resetting. Takes precedence over `resetOnDoubleClick`.
0.2Zoom step applied by the zoom-in/zoom-out buttons, `+`/`-` keys, and double-click-to-zoom.
Plus all standard <div> HTML
attributes.