Phoenix LiveView function component that mounts a Fresco viewer.
Renders a host <div> containing a stage <div> and an <img>. The
companion JS hook (FrescoViewer in priv/static/fresco.js) attaches
Pointer Events for unified mouse/touch/pen gestures, applies
transform: translate3d(tx, ty, 0) scale(s) on the stage element, and
publishes a handle to window.Fresco.viewerFor(id) so peer extensions
(Tessera, future Etcher) can attach.
The image is server-rendered inside the host so it appears immediately —
the hook reads naturalWidth/Height on mount and fits the image into
the viewport without any "blank box" flash.
Usage
<Fresco.viewer
id="photo"
src={~p"/uploads/photo.jpg"}
class="w-full h-[80vh] rounded"
/>Interactions
- Pan: click/touch drag, arrow keys (after focusing the viewer)
- Zoom: mouse wheel (centered on cursor), pinch (two-finger on touch
or trackpad), double-click (2× centered on cursor),
+/-keys - Reset: nav button,
0key — fits the image to the viewport - Fullscreen: nav button,
fkey — toggles native browser fullscreen
Parent app setup
Import the JS hook and spread FrescoHooks into your LiveSocket hooks:
import "../../deps/fresco/priv/static/fresco.js"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...window.FrescoHooks, ...colocatedHooks }
})
Summary
Functions
Renders a Fresco viewer for the given image source.
Functions
Renders a Fresco viewer for the given image source.
Companion JS hook attaches gesture handlers, fits the image to the viewport, and publishes the handle for peer extensions.
Attributes
id(:string) (required) - DOM id; must be unique on the page.src(:string) (required) - URL of the image to display.The default behavior treats
srcas a plain image URL. Extensions can register source providers viawindow.Fresco.registerSourceProvider/2to intercept specific URL patterns; the bundled engine handles{type: "image"}sources and throws a clear error for anything else (Tessera-style tile sources are planned for a later release).class(:string) - CSS classes for the viewer container. Defaults to"w-full h-96".infinite_canvas(:boolean) - Whentrue, drops the default "image must cover viewport" clamp so the user can pan freely beyond the image edges and zoom out until the image is a thumbnail in the middle of an empty canvas. The viewer's background dot-grid (always present) becomes visible in the void around the image so it reads as "canvas," not "broken layout." Defaultfalsekeeps the stock single-image viewer behavior — pan stays location-locked inside the image, zoom-out floor is fit-to-viewport.Pairs naturally with future layered overlays (e.g. Etcher annotations) that need to draw shapes, callouts, or labels in the white space around the image, Figma/Miro/Excalidraw style.
Defaults to
false.theme(:atom) - Color scheme for the viewer host background, dot grid, and nav buttons.:system(default) — follow the OS / browserprefers-color-scheme.:light— force light palette regardless of OS preference.:dark— force dark palette regardless of OS preference.:inherit— emit only the host structure; the parent app's CSS supplies the six--fresco-*custom properties. Use this to wire Fresco to a parent theme system (daisyUI, custom palettes, …).
Theming is implemented as CSS custom properties on
.fresco-viewer(--fresco-bg,--fresco-grid-dot,--fresco-nav-bg,--fresco-nav-bg-hover,--fresco-nav-fg,--fresco-nav-focus).Defaults to
:system. Must be one of:system,:light,:dark, or:inherit.zoom_floor(:float) - Optional minimum zoom scale, in engine units (screen-px-per-image-px). When set, the engine clamps every zoom path — wheel, pinch, double-click,fitBounds— at this floor.nil(default) falls through to the engine's normal floor (sFitin clamped mode,sFit * 0.05in:infinite_canvasmode).Consumers can also set this at runtime via
handle.setZoomFloor(scale)— useful when the floor depends on state that's known only after mount (e.g. a paged reader recomputing the floor each time the user navigates to a new page).Defaults to
nil.zoom_ceiling(:float) - Optional maximum zoom scale. Symmetric to:zoom_floor.nil(default) uses the engine's default ceiling (8× natural pixel ratio capped by the 8192-px raster safety limit).Defaults to
nil.pan_locked(:boolean) - Whentrue, single-pointer pan gestures (mouse drag, touch drag, arrow keys) are suppressed. Two-pointer pinch still works for zoom. Useful for paged readers that want the page statically centered at fit and only allow pan once the user has zoomed in.Toggle at runtime via
handle.setPanLocked(true|false)— typically in response to ahandle.on("zoom", …)subscription that flips the lock as the user crosses the fit threshold.Defaults to
false.gestures(:list) - Allowlist of enabled gestures. Atom list:[:pan, :pinch, :wheel, :double_click, :keyboard]. Defaultnilenables all. Omitted entries are disabled.Defaults to
nil.nav_buttons(:list) - Allowlist of enabled built-in nav buttons. Atom list:[:home, :zoom_in, :zoom_out, :fullscreen]. Defaultnilenables all. Omitted entries are hidden.Defaults to
nil.Global attributes are accepted.