Skip to main content

preview-panel

A pan-zoom container with controls, fullscreen dialog, and synced state between views.

About

  • Pan & zoom via drag, scroll wheel, and pinch-to-zoom gestures, behind a lock so the page scrolls past the panel until the user unlocks it
  • An actions slot for your own buttons beside the zoom controls
  • Built-in zoom controls with percentage badge
  • Zero re-renders during continuous interactions (ref-based transforms)
  • unsafeHtml prop for rendering raw HTML/SVG content directly (caller-sanitized; XSS sink)
  • Fullscreen dialog variant with PreviewPanelDialog
  • Synced state between inline and dialog panels via syncPanels
  • RAF-batched state emission for maximum performance
  • Built with gentleduck/ui components: Button, Badge, ButtonGroup, Separator, Tooltip, Dialog

Philosophy

Preview panels bridge the gap between list views and detail views. Instead of navigating away to see content, users get an inline preview that respects their current context. We ship this as a compound component because the trigger, content, and dialog modes each need independent control while sharing state.

How It's Built

Loading diagram...

Installation


npx @gentleduck/cli add preview-panel

npx @gentleduck/cli add preview-panel

Usage

import { PreviewPanel } from '@/components/ui/preview-panel'
<PreviewPanel maxHeight="400px">
  <YourContent />
</PreviewPanel>

Locking

Drag, wheel and pinch start locked, so scrolling a page past the panel scrolls the page. The lock button in the controls turns them on, and the zoom buttons work either way. Start unlocked where the panel is the page's main content:

<PreviewPanel defaultLocked={false} maxHeight="400px">
  <YourContent />
</PreviewPanel>

With actions

actions renders in the toolbar before the zoom controls, and still renders with showControls={false}.

<PreviewPanel actions={<Button size="icon-sm" icon={<Copy />} aria-label="Copy" />}>
  <YourContent />
</PreviewPanel>

With HTML content

<PreviewPanel unsafeHtml="<svg>...</svg>" maxHeight="400px" />

With dialog and sync

import { PreviewPanelDialog } from '@/components/ui/preview-panel'
<PreviewPanelDialog syncPanels maxHeight="400px">
  <YourContent />
</PreviewPanelDialog>

Examples

HTML / SVG content

Render raw HTML or SVG strings directly using the unsafeHtml prop. The value is rendered verbatim via dangerouslySetInnerHTML and is not sanitized, so only pass HTML you fully trust.

Dialog with synced zoom

Open the content in a fullscreen dialog. Zoom and position sync between both panels, and the fullscreen panel starts unlocked.

Image viewer

Use as a zoomable image viewer.

RTL Support

Set the direction once with DirectionProvider (or <html dir="rtl">) and the panel follows: the zoom controls move to the top-left corner and lay out RTL. Use zoomInText, zoomOutText, resetText, lockText, and unlockText to translate the control labels.

Motion

Use MotionPreviewPanel for a smooth entrance animation powered by motion. The panel uses a scaleIn entrance (fade, scale, and blur) with a bouncy spring transition on mount, and its internal controls (buttons, separators, badges, tooltips) use the full motion stack.

API Reference

PreviewPanel

The core pan-zoom container.

PropTypeDefaultDescription
dir'ltr' | 'rtl'-Direction override. Defaults to the nearest DirectionProvider, then <html dir>.
maxHeightstring--Maximum height of the panel container (e.g. "400px")
minZoomnumber0.25Minimum zoom level
maxZoomnumber4Maximum zoom level
initialZoomnumber1Starting zoom level
showControlsbooleantrueWhether to show the zoom controls overlay
defaultLockedbooleantrueWhether drag, wheel and pinch start locked, so the wheel scrolls the page. The zoom buttons work either way
actionsReact.ReactNode--Rendered in the toolbar before the zoom controls
unsafeHtmlstring--Raw HTML string rendered verbatim via dangerouslySetInnerHTML. Takes priority over children. Not sanitized: pass only HTML you fully trust, never user-, CMS-, or URL-derived markup
childrenReact.ReactNode--Content to render inside the panel
classNamestring--Additional CSS classes for the outer container
styleReact.CSSProperties--Inline styles for the outer container
onStateChange(state: PreviewPanelState) => void--Called whenever zoom or position changes. Use to sync with another panel
syncStatePreviewPanelState--External state to apply. The panel syncs to this state when set
zoomInTextstring'Zoom in'Label and tooltip for the zoom-in control
zoomOutTextstring'Zoom out'Label and tooltip for the zoom-out control
resetTextstring'Reset view'Label and tooltip for the reset control
lockTextstring'Lock the view'Label and tooltip for the lock control while the view is unlocked
unlockTextstring'Unlock the view'Label and tooltip for the lock control while the view is locked
...propsReact.HTMLProps<HTMLDivElement>-Additional props to spread to the content div

MotionPreviewPanel

scaleIn entrance (fade, scale, and blur) with springBouncy transition on mount. Zoom controls use MotionButton, MotionBadge, MotionSeparator, and MotionTooltip internally. Requires the motion package.

PropTypeDefaultDescription
...propsPreviewPanelProps-All props from PreviewPanel are supported

PreviewPanelDialog

Combines an inline PreviewPanel with a fullscreen Dialog. The expand button sits in the inline panel's toolbar, before the zoom controls. The inline panel starts locked and the fullscreen one unlocked.

PropTypeDefaultDescription
childrenReact.ReactNode-Content rendered in both inline and dialog panels
unsafeHtmlstring-Raw HTML string rendered verbatim via dangerouslySetInnerHTML. Takes priority over children. Not sanitized: pass only HTML you fully trust
classNamestring-Class name for the outer wrapper
panelClassNamestring-Class name applied to both PreviewPanel instances
maxHeightstring-Maximum height of the inline panel
minZoomnumber0.25Minimum zoom level
maxZoomnumber4Maximum zoom level
initialZoomnumber1Starting zoom level
showControlsbooleantrueWhether to show zoom controls
defaultLockedbooleantrueWhether the inline panel starts locked. The fullscreen panel starts unlocked
syncPanelsbooleantrueWhether to sync zoom and position between inline and dialog panels
fullscreenTextstring'Open fullscreen'Label and tooltip for the expand button
zoomInTextstring'Zoom in'Label and tooltip for the zoom-in control
zoomOutTextstring'Zoom out'Label and tooltip for the zoom-out control
resetTextstring'Reset view'Label and tooltip for the reset control
lockTextstring'Lock the view'Label and tooltip for the lock control while the view is unlocked
unlockTextstring'Unlock the view'Label and tooltip for the lock control while the view is locked

PreviewPanelState

The state object used for syncing between panels.

type PreviewPanelState = {
  zoom: number
  x: number
  y: number
}