Zumly
Zoom-based navigation for the web. Turn any site into a spatial zoom interface with just HTML
Install / Use
/learn @zumerlab/ZumlyREADME
Status
Zumly is under active development. The core stack is stable: depth and lateral navigation, pluggable transition drivers (CSS, WAAPI, none, Anime.js, GSAP, Motion, custom), unified nav UI (depth + lateral, eight positions), view resolver and prefetch cache, optional plugin API (<code>.use()</code>), and the hash router plugin. View sources include HTML strings, URLs, async functions, objects with <code>render()</code>, DOM nodes, and web component tags.
Zoom-out geometry uses batched DOM reads plus pure math where possible to cut layout thrash before animations (see Geometry optimization).
Docs: Roadmap & topics · Transition drivers · Geometry notes
Overview
Unlike free-pan ZUIs, Zumly focuses on discrete, hierarchical navigation: users zoom into a focused element (<code>.zoom-me</code>) to open the next view, so attention (focus) and depth (Z) stay aligned with layout (XY).
The engine is UI-agnostic—you supply markup and CSS. Transforms and timing are handled for you; design systems and frameworks integrate by resolving each view to a DOM subtree (see View sources and Framework integration below).
What Zumly is
Zumly is not a freeform zooming canvas or map-like navigation system. It is a discrete, hierarchical zoom interface: screens are views at different depths, connected by triggers, with continuous motion between them.
It fits especially well when:
- you want focus-driven flow (zoom into what matters)
- spatial context between parent and child should persist
- you are building menus, stories, dashboards, or exploratory UIs without a classic router-only metaphor
Installation
NPM
npm install zumly
# or
yarn add zumly
CDN
Include Zumly in your project via a <script> tag from unpkg.com/zumly.
Direct download
Download the built files from unpkg.com/zumly (see the dist folder).
Setup
Browser bundle (global)
- Add the CSS in your
<head>:
<link rel="stylesheet" href="zumly/dist/zumly.css">
<!-- or https://unpkg.com/zumly/dist/zumly.css -->
- Load the JS bundle (it exposes
window.Zumly):
<script src="zumly/dist/zumly.js"></script>
<!-- or https://unpkg.com/zumly/dist/zumly.js -->
Hello World
- Add a container with the class
zumly-canvas:
<div class="example zumly-canvas"></div>
- Create your views and start Zumly:
const hello = `
<div class="z-view">
H E L L O <br>
W <span class="zoom-me" data-to="world">O</span> R L D!
</div>
`;
const world = `
<div class="z-view">
<img src="https://raw.githubusercontent.com/zumly/website/gh-pages/images/world.png" alt="World">
</div>
`;
const app = new Zumly({
mount: '.example',
initialView: 'hello',
views: { hello, world },
});
await app.init();
- Live example: CodePen
Options
Zumly constructor:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| mount | string | Yes | CSS selector for the canvas element (must have class zumly-canvas). |
| initialView | string | Yes | Name of the first view to show. |
| views | object | Yes | Map of view names to view sources (see View sources below). |
| preload | string[] | No | View names to resolve and cache when the app initializes. |
| transitions | object | No | Duration, ease, cover, driver, effects, stagger, hideTrigger for zoom transitions. |
| deferred | boolean | No | Defer content rendering until after animation completes (default: false). |
| debug | boolean | No | Enable debug messages (default: false). |
| lateralNav | boolean | object | No | Lateral navigation UI: { mode, arrows, dots, keepAlive, position }. |
| depthNav | boolean | object | No | Depth back button: { position }. Default: 'bottom-left'. |
| inputs | boolean | object | No | Input methods: { click, keyboard, wheel, touch }. |
| componentContext | object | No | Context passed to component-style views. |
Transitions (optional):
transitions: {
driver: 'css', // 'css' | 'waapi' | 'anime' | 'gsap' | 'motion' | 'none' or custom function(spec, onComplete)
cover: 'width', // or 'height' — how the previous view scales to cover the trigger
duration: '1s',
ease: 'ease-in-out',
effects: ['blur(3px) brightness(0.7)', 'blur(8px) saturate(0)'], // CSS filters for [previous, last] background views
stagger: 0, // delay (ms) between layers during transition
hideTrigger: false, // false | true (visibility:hidden) | 'fade' (opacity crossfade)
// threshold: { enabled: true, duration: 300, commitAt: 0.5 } // parsed but not wired in the engine yet
}
transitions.parallax is accepted for compatibility but not applied (reserved; intensity is fixed to 0 in the engine).
Transition drivers: Zoom animations are handled by a pluggable driver (transitions.driver). You can swap implementations without changing app logic. To author your own, see docs/DRIVER_API.md and the zumly/driver-helpers export.
| Driver | Description |
|--------|-------------|
| 'css' (default) | CSS keyframes and animationend; uses zumly.css variables. |
| 'waapi' | Web Animations API (element.animate()). No extra dependency. |
| 'none' | No animation; applies final state immediately. Useful for tests or instant UX. |
| 'anime' | Anime.js — requires global anime (load from CDN before use). |
| 'gsap' | GSAP — requires global gsap (load from CDN before use). |
| 'motion' | Motion — requires global Motion (load from CDN before use). |
| function(spec, onComplete) | Custom driver. Receives { type, currentView, previousView, lastView, currentStage, duration, ease } and must call onComplete() when done. |
Example with instant transitions (e.g. for tests):
const app = new Zumly({
mount: '.canvas',
initialView: 'home',
views: { home, detail },
transitions: { driver: 'none', duration: '0s' },
});
Lateral navigation (arrows + dots bar):
lateralNav: true // mode: 'auto' (default), bottom-center
lateralNav: false // disabled
lateralNav: { mode: 'always' } // always show when siblings exist
lateralNav: { mode: 'auto', dots: false } // auto mode, no dots
lateralNav: { position: 'top-center' } // top instead of bottom
| Mode | Description |
|------|-------------|
| 'auto' (default) | Shows lateral nav only when the current view doesn't cover the full canvas — preserving spatial context. |
| 'always' | Always shows lateral nav when siblings exist, regardless of coverage. |
Position: 'bottom-center' (default) or 'top-center'.
Depth navigation (back button):
depthNav: true // default: back button at bottom-left
depthNav: false // disabled
depthNav: { position: 'top-left' } // top instead of bottom
Position: 'bottom-left' (default) or 'top-left'. The depth and lateral nav are separate, independently positioned components.
Zoomable elements:
- Give the view root the class
z-view. - Add class
zoom-meanddata-to="viewName"to the element that triggers zoom-in. - Per-trigger overrides via
data-*attributes:
| Attribute | Description |
|-----------|-------------|
| data-to | Required. Target view name. |
| data-with-duration | Override transition duration (e.g. "2s"). |
| data-with-ease | Override easing function. |
| data-with-cover | Override cover dimension ("width" or "height"). |
| data-with-stagger | Override stagger delay in ms (e.g. "100"). |
| data-with-effects | Override effects (pipe-separated: "blur(5px)\|blur(10px)"). |
| data-hide-trigger | Override hideTrigger ("fade" or presence = hide). |
| data-deferred | Override deferred rendering (presence = true). |
| data-* | Any other data attribute becomes a prop in ViewContext.props. |
<div class="z-view">
<div class="zoom-me" data-to="detail"
data-with-duration="2s"
data-with-ease="ease-in"
data-with-cover="height"
data-with-stagger="100"
data-id="42">
Zoom in
</div>
</div>
View sources
Each entry in views is a view source. The resolver detects the type and resolves to a DOM node. Hyphenated view names (e.g. 'my-dashboard') are resolved as keys in views first; only raw template strings with a hy
