Zumly + Orbit
Orbit places elements around a center: rings, arcs, gauges, radial menus. Zumly zooms into an element to open the next view. Together, every satellite of a dial can open its own dial, and the satellites of one ring are siblings you can move between.
Rooms are satellites, and each one opens its own dial. Open a device, move the slider and come back: the room's gauge repaints.
How they divide the work
Neither library knows about the other, and no glue code is needed. Orbit lays out and draws what is inside a view. Zumly moves between views.
| Who | Does |
|---|---|
| Orbit | Sizes a layout from its .bigbang, places satellites on rings, draws <o-arc> and <o-progress>, moves .depth layers in 3D. |
| Zumly | Opens a view from a trigger, keeps the parent around it, moves to siblings, zooms back out. |
| You | Mark the satellites that open something with zoom-me and data-to, and keep the app state outside the views. |
Orbit redraws by itself when its elements are inserted, resized or changed, so a view that Zumly renders, moves, hides behind a sibling or brings back draws correctly with every Zumly driver.
Install
npm install zumly @zumer/orbit
import Zumly from 'zumly' import 'zumly/style.css' import '@zumer/orbit/style' import '@zumer/orbit'
Or from a CDN:
<link rel="stylesheet" href="https://unpkg.com/zumly/dist/zumly.css"> <link rel="stylesheet" href="https://unpkg.com/@zumer/orbit/dist/orbit.min.css"> <script src="https://unpkg.com/zumly/dist/zumly.js"></script> <script defer src="https://unpkg.com/@zumer/orbit/dist/orbit.min.js"></script>
This guide uses Orbit 2, where a layout takes its size from its .bigbang box. With
Orbit 1.x, set the size on the .gravity-spot instead:
--o-force: 600px.
A dial that opens dials
Put a .bigbang inside a view and give the view a size. The layout fills the
view. Make each satellite's capsule a trigger:
const dial = (title, items, className = 'z-view dial') => `
<div class="${className}">
<div class="bigbang">
<div class="gravity-spot">
<div class="orbit-10" style="--o-size-ratio: 1.5">
${items.map(() => '<o-arc></o-arc>').join('')}
</div>
<div class="orbit-7">
${items.map(item => `
<div class="satellite">
<div class="capsule zoom-me" data-to="${item}">${item}</div>
</div>`).join('')}
</div>
<div class="orbit-0" style="--o-size-ratio: 6">
<div class="satellite at-center"><div class="capsule">${title}</div></div>
</div>
</div>
</div>
</div>`
const app = new Zumly({
mount: '.canvas',
initialView: 'home',
views: {
// The first view fills the canvas and centers its dial
home: `<div class="z-view home">${dial('Home', ['kitchen', 'living', 'garage'], 'dial')}</div>`,
kitchen: dial('Kitchen', ['oven', 'lights']),
living: dial('Living', ['tv', 'lights']),
garage: dial('Garage', ['door']),
// …
},
transitions: { duration: '700ms', ease: 'cubic-bezier(.2,.8,.2,1)' }
})
app.init()
.home { width: 100%; height: 100%; display: grid; place-items: center; }
.dial { width: min(80vmin, 600px); aspect-ratio: 1; }
.dial .capsule { width: 100%; height: 100%; border-radius: 50%; display: grid; place-items: center; }
Zooming in scales the dial until the capsule you clicked covers the next dial, so each
level grows out of its satellite. The capsules on orbit-7 are siblings:
the sibling bar, a click on a neighbor that is still visible, and goTo(name, { mode: 'lateral' })
move around the ring without going back up.
The live example at the top of this page is this dial, complete: open it on its own to read the source.
Which element is the trigger
Zumly zooms from the box of the trigger element. Use an element whose box matches what the user sees:
| Trigger | Result |
|---|---|
.capsule or .satellite | Exact: the next view grows out of the satellite. Recommended. |
The center (.satellite.at-center > .capsule) | Exact: good for "open the detail of this dial". |
<o-arc> or <o-progress> | The box of an arc is its whole ring, so the next view grows out of the full dial, not the segment. To open from a segment, put a satellite on the segment and make it the trigger. |
A clickable band that should not open a view (for example to change a value) is an
<o-arc interactive> without zoom-me. Clicks inside the current view that do not
land on a trigger are left to your code.
State and repainting
Views behind the current one stay in the DOM and are not rendered again. If a value changes while the user is in a detail view, the gauge in the parent still shows the old value when they come back. Keep the values outside the views and repaint when a view becomes current again:
const state = { battery: 72 }
const views = {
car: ({ target, onActivate }) => {
target.className = 'z-view dial' // target is the view; paint() fills its content
const paint = () => {
target.innerHTML = `
<div class="bigbang"><div class="gravity-spot">
<div class="orbit-10" style="--o-size-ratio: 2">
<o-progress value="${state.battery}" shape="rounded"></o-progress>
</div>
<div class="orbit-0" style="--o-size-ratio: 6"><div class="satellite at-center">
<div class="capsule zoom-me" data-to="battery">${state.battery}%</div>
</div></div>
</div></div>`
}
paint()
onActivate(paint)
},
battery: ({ target }) => { /* a slider that writes state.battery */ }
}
To update only the value, change the attribute instead of the markup:
target.querySelector('o-progress').setAttribute('value', state.battery). Orbit redraws it.
To open a view from code, for example when an alert arrives, call app.zoomIn(element)
with the satellite: the view opens from it. app.zoomTo(name) opens from the center
of the canvas.
Depth layers inside a view
Orbit's .depth layers (rings stacked in 3D with data-depth) work inside a Zumly
view. They are a different kind of depth: Orbit layers are parts of one object, Zumly
levels are places. Keep them visually distinct. A pattern that works:
- Zumly levels dim and blur when they go back:
transitions.effects: ['blur(3px) brightness(.45)', 'blur(8px) brightness(.25)']. - Orbit layers never blur; they move in z and keep their size.
- A click on a back layer brings it to the front (your code changes
data-depth). It does not zoom out, because Zumly leaves clicks inside the current view to you. - The front layer's center is the trigger to the next level.
To tilt the whole object with the pointer, add outer-perspective next to
depth and set the perspective on your own wrapper.
Touch and gestures
A swipe to the left zooms out. Knobs, dials you drag and rings you spin need that gesture
for themselves: add data-no-swipe to them. Sliders and text fields are excluded
automatically. Give touch targets room: <o-arc interactive hit-width="12"> adds
12px of touch area on each side of a band.
Full-screen dials leave no parent visible around them, so "tap outside to go back" has no
outside. Keep the back button (depthNav) or make the views smaller than the canvas.
Design tips
- Keep the identity in the center. The center of each dial says where the user is.
- One ring of triggers per view. Its satellites are the siblings; other rings are information.
- Size by the view. The
.bigbangtakes the view's width; size views withmin()orvminso dials fit on phones. - Reduced motion. Zumly makes transitions instant and Orbit disables its depth animation when the user prefers reduced motion.
See it in action
The complete dial example, with dials that open dials.
Open this example ↗ Learn in six steps