Skip to content
zumerlabPublic

About

πŸ’« HTML on circles. A CSS layout system for radial UIs: gauges, dials, knobs, radial menus, charts and watch faces.

Topics

Resources

Code of conduct

Contributing

Stars

768 stars

Watchers

7 watching

Forks

Repository files navigation

Orbit

Orbit

Orbit puts HTML on circles. Gauges, dials, knobs, radial menus, donut charts, watch faces, sci-fi panels: anything that grows around a center instead of down a page.

You write plain HTML with a few classes. CSS does the geometry and the look; a small runtime keeps numbering, spacing and SVG arcs right as things change. It works the same in plain HTML, React, Vue, Svelte or anything else that renders the DOM.

Documentation and examples Β· npm

Install

npm install @zumer/orbit
import '@zumer/orbit/style'
import '@zumer/orbit'

Or, in a plain page:

<link rel="stylesheet" href="https://unpkg.com/@zumer/orbit/dist/orbit.min.css">
<script defer src="https://unpkg.com/@zumer/orbit/dist/orbit.min.js"></script>

Your first universe

Save this as index.html and open it. It draws a progress ring with a label in the middle, and the whole thing is as big as its 240px box.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A first Orbit</title>
  <link rel="stylesheet" href="https://unpkg.com/@zumer/orbit/dist/orbit.min.css">
  <script defer src="https://unpkg.com/@zumer/orbit/dist/orbit.min.js"></script>
  <style>
    body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: system-ui, sans-serif; }
    .gauge { width: 240px; }
    .gauge o-progress { --o-fill: #3da9fc; --o-back-fill: #1d3749; --o-stroke: none; }
    .gauge .value { font-size: 40px; font-weight: 600; }
  </style>
</head>
<body>
  <div class="gauge bigbang">
    <div class="gravity-spot">
      <div class="orbit-10" style="--o-size-ratio: 2">
        <o-progress value="72" shape="rounded" role="progressbar"
          aria-label="Upload" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100"></o-progress>
      </div>
      <div class="orbit-0" style="--o-size-ratio: 12">
        <div class="satellite at-center"><div class="capsule value">72%</div></div>
      </div>
    </div>
  </div>
</body>
</html>

Change width on .gauge and everything follows: the ring, its thickness, the space for the label. Change value and the ring redraws.

How Orbit thinks

Orbit borrows its words from a solar system. It sounds like a gimmick for about a minute; after that it is the easiest way to remember what each piece does.

First there is a big bang. A .bigbang creates the universe: the space your layout lives in. Its box is the only size you ever set. It is square by default, as wide as its parent, and a parent with a height keeps it from growing taller.

Then gravity. Inside it, a .gravity-spot is the center of mass. Its gravity reaches as far as the shorter side of the universe; that reach is --o-force, and it is the diameter of the outermost orbit, orbit-12.

Gravity draws orbits. From the center to that edge there are 12 lanes. orbit-1 is the first lane, orbit-6 is halfway out, orbit-12 touches the edge. orbit-0 is the center itself, and orbit-13 to orbit-24 exist too, past the edge, for things that should stick out.

Satellites travel the orbits. A .satellite sits on a ring, and Orbit spreads all the satellites of a ring around it on its own. Each one carries a .capsule, where your content goes, kept upright however far around the ring it is.

And there is more up there. A .vector is a ray pointing out from the center: ticks, needles. A .side is a chord between neighbors: polygons. <o-arc> draws a piece of a lane, with text along it if you want. <o-progress> fills a lane up to a value.

.bigbang                          the universe: its box sets the size
└── .gravity-spot                 the center; reaches the shorter side
    β”œβ”€β”€ .orbit-N                  a lane, N twelfths of the way out
    β”‚   β”œβ”€β”€ .satellite > .capsule ← your content
    β”‚   β”œβ”€β”€ .vector Β· .side       rays and chords
    β”‚   └── <o-arc> Β· <o-progress>
    └── .orbit-M …

That is all the structure there is. A few rules keep it working:

  • Rings are direct children of a .gravity-spot, never of another ring.
  • A ring holds only .satellite, .vector, .side, <o-arc> and <o-progress>.
  • Content goes in a .capsule inside a .satellite. Centered content goes in .orbit-0 > .satellite.at-center > .capsule.

Add dev-orbit to the .bigbang while you build. It draws the lanes, outlines anything in the wrong place and explains it in the console.

Size is a fraction of the universe

Every element on a ring is one lane thick: a twelfth of the radius. A satellite on orbit-8 is a circle one lane across; a band of <o-progress> is one lane wide. To make things thicker or thinner, set --o-size-ratio, in lanes, on the element, on its ring, or on the whole .gravity-spot:

<div class="orbit-8" style="--o-size-ratio: 2">
  <div class="satellite"><div class="capsule">A</div></div>
  <div class="satellite"><div class="capsule">B</div></div>
  <div class="satellite"><div class="capsule">C</div></div>
</div>

An element on orbit-N reaches half its thickness past lane N. So a band two lanes thick on orbit-11 ends exactly at the edge of the universe.

Because everything is a fraction of one box, an Orbit layout is responsive without any code. Put it in a sidebar, a card or a full screen and it scales as one piece. If you need a fixed size instead, give the reach a length: .gravity-spot { --o-force: 400px }.

Universes inside universes

A satellite is a universe too. Put a .gravity-spot inside one and its reach is the satellite's edge, like a moon system around a planet:

<div class="orbit-9" style="--o-size-ratio: 4">
  <div class="satellite">
    <div class="gravity-spot">
      <div class="orbit-12"><o-progress value="40"></o-progress></div>
    </div>
  </div>
</div>

Every .gravity-spot starts from the defaults, so a nested system does not inherit the sizes or angles of the one around it.

Angles

Angles start at the top and grow clockwise. --o-range is how much of the circle a ring uses, and --o-from where it starts:

<div class="orbit-10" style="--o-range: 240deg; --o-from: 240deg">…</div>

Classes cover the common values: range-180, from-270, ccw to go counterclockwise, and fit-range to put the first and last satellite or tick on the two ends of a partial range.

Property Default Set it on What it does
--o-force 100cqmin .gravity-spot Reach: the diameter of orbit-12.
--o-size-ratio 1 system, ring or element Thickness, in lanes.
--o-range 360deg system or ring How much of the circle the ring uses.
--o-from 0deg system or ring Where the range starts.
--o-gap 1 system, ring or arc Space between sibling arcs.
--o-alignment 0px element Moves an element in (positive) or out (negative) from its lane. Also set by .inner-orbit and .outer-orbit.

Values cascade, so set them once on the .gravity-spot for the whole system or on a ring for its elements.

Which way is out

Each satellite tells its content where it sits: --o-dir-x and --o-dir-y are the direction from the center, from -1 to 1 (y grows down, and both are 0 on the center satellite). It is handy for labels that should hang outward, above the satellites of the upper half and below the rest:

.label {
  /* -1 in the upper half, 1 in the lower half */
  --side: clamp(-1, var(--o-dir-y) * 1000, 1);
  position: absolute; left: 50%;
  top: calc(50% + var(--side) * (50% + 4px));
  translate: -50% calc((var(--side) - 1) * 50%);
}

Arcs and progress

<o-arc> and <o-progress> go straight into a ring. Both take a value out of max (100 by default) and an ending: shape="rounded", circle, arrow, slash, zigzag and a few more, all scaled to the band.

Without value, sibling arcs split the ring between them: that is a segmented ring or a pie. With value, they stack one after another: that is a donut chart.

<div class="orbit-10" style="--o-size-ratio: 3">
  <o-arc value="45" style="--o-fill: #3da9fc"></o-arc>
  <o-arc value="30" style="--o-fill: #ef4565"></o-arc>
  <o-arc value="25" style="--o-fill: #90b4ce"></o-arc>
</div>

Text inside an arc is drawn along it. With range="auto" the arc is as long as its text, and consecutive auto arcs follow each other, which makes curved menus easy. text-anchor="middle" centers an auto arc on its angle:

<div class="orbit-11 from-300">
  <o-arc range="auto">Home</o-arc><o-arc range="auto">Projects</o-arc><o-arc range="auto">About</o-arc>
</div>

Progress rings draw filled bands. For a thin gauge line, use variant="stroke": a 2px line over a gray track, colored with --o-stroke and --o-back-stroke.

Arcs and progress are decorative until you say otherwise. interactive makes the whole band clickable, transparent or not, and hit-width="12" adds 12px of touch area on each side:

<div class="orbit-10" style="--o-size-ratio: 2">
  <o-arc interactive hit-width="8" style="--o-fill: transparent" aria-label="Volume"></o-arc>
</div>

You bring the keyboard behavior and the accessible names for any control you build. Arcs are placed with the rotate property, so a transform or scale of yours adds to it instead of replacing it.

Depth

A system can stack its rings in 3D. Add depth to the .gravity-spot and each ring becomes a layer: data-depth="1" sends it one step back, .layer-front marks the one in front. Layers keep their apparent size, because the scale makes up for the perspective, and they animate when their depth changes.

<div class="gravity-spot depth" style="--o-depth-grow: .42">
  <div class="orbit-4 layer-front"><o-progress value="82"></o-progress></div>
  <div class="orbit-4" data-depth="1"><o-progress value="40"></o-progress></div>
  <div class="orbit-4" data-depth="2"><o-progress value="65"></o-progress></div>
</div>
Property Default What it does
--o-z depth Γ— step Position on the z axis in unitless pixels; negative is farther. Overrides data-depth.
--o-depth-step -110 Distance between steps.
--o-perspective 900 Perspective, in unitless pixels.
--o-depth-grow 0 Extra size per step back: .42 gives 1Γ—, 1.42Γ—, 1.84Γ—.
--o-depth-compensate 1 0 lets farther layers look smaller.
--o-back-opacity 1 Opacity of the layers behind the front one.
--o-depth-duration / --o-depth-easing .8s / cubic-bezier(.2,.8,.2,1) The depth transition. Off with prefers-reduced-motion.

A layer can be a whole system, ring, label and face together: make it a nested .gravity-spot with data-depth. To tilt the whole object from outside (with the pointer, say), add outer-perspective next to depth and set --o-perspective to the perspective of the ancestor that owns it. The depth demo shows both.

In an app

Orbit watches the DOM for you: added, removed and reordered elements, attribute and style changes, and resized containers. Updates are batched into the next animation frame.

The package is safe to import during server rendering; import it in the browser entry too, so it can register the elements and watch the page. It exports Orbit, OrbitArc, OrbitProgress and registerOrbit; registration happens on import and calling registerOrbit() again is harmless.

A few things for when you need them:

import { Orbit } from '@zumer/orbit'

await Orbit.ready()                 // every pending arc is drawn
gauge.addEventListener('o-rendered', () => measure(gauge))  // one arc redrew
Orbit.refresh(panel)                // update now, e.g. after editing a stylesheet through CSSOM

Orbit.resize(target) only matters for systems with a fixed px --o-force: it scales them with the target's width, taking 500px as 1Γ—, and returns a function that stops it.

For shadow DOM, include the stylesheet inside each shadow root. Arcs and progress register their shadow root on their own; for a shadow tree with only CSS elements, call Orbit.refresh(shadowRoot) once.

Override Orbit through the documented custom properties. The runtime manages data-orbit-ring, --o-layout-* and --o-arc-start itself.

TypeScript and editors

The package ships types for the API and for document.querySelector('o-progress'). For JSX, load the element types once, for example in a .d.ts file:

import type {} from '@zumer/orbit/react'

@zumer/orbit/custom-elements.json is a Custom Elements Manifest. For attribute completion in VS Code:

{ "html.customData": ["./node_modules/@zumer/orbit/dist/vscode.html-custom-data.json"] }

When something looks wrong

Orbit explains common mistakes in the browser console, starting with [orbit]:

You see Because Fix
Nothing, and the console says has no size The .bigbang is 0px wide or tall, usually in a parent that shrinks to fit. Give the .bigbang or its parent a width.
A layout as big as the window The .gravity-spot is not inside a .bigbang, so it measures the viewport. Wrap it in a .bigbang.
An empty arc or ring value is not a number, or max is not positive. Fix the attribute; the console names it.
Content overlapping or off the ring It sits directly in a ring instead of a .satellite. Add dev-orbit to see where, then wrap it.
Curved text cut off The text is longer than its arc. Shorten it, widen --o-range, use range="auto", or fit-range.
Everything thin Elements are one lane thick. Raise --o-size-ratio on the ring or the system.
An arc that ignores clicks Arcs are decorative by default. Add interactive, and hit-width for a bigger target.

The reset and the default theme have zero specificity, so a single class of yours always wins, also inside a .capsule. theme-cyan gives a ready-made cyan look and combines with dev-orbit.

Orbit needs CSS sin(), cos() and container query units. Without JavaScript, a CSS fallback still places simple layouts (up to 24 rings and 60 children per type; it also needs :has() and :nth-child(… of …)); <o-arc> and <o-progress> always need the runtime.

Coming from 1.x

Version 2 changes where size comes from. In 1.x every system was 500px across, whatever the size of its .bigbang, and you needed Orbit.resize() to make it responsive. Now orbit-12 reaches the edge of the .bigbang.

  • To keep the 1.x size, set .gravity-spot { --o-force: 500px }. Orbit.resize() still scales those systems.
  • To move to the new model, remove --o-force and Orbit.resize() and pick rings by their share of the space. A 1.x orbit-N in a box W px wide lands near orbit-(N Γ— 500 / W), and its thickness near --o-size-ratio: 500 / W.
  • A .bigbang is a size container, so it is the containing block of position: fixed descendants. Directly in <body> it is capped at the viewport height instead of forced to it.
  • .satellite no longer resets --o-size-ratio, so a value on its ring reaches it. shrink-N on a ring only changes how close it sits to the previous lane.
  • --o-gap cascades from the ring and the system. --o-aligment still works; --o-alignment is the right spelling.
  • fit-range on a ring of arcs no longer overflows the range.
  • Satellites have no border by default. The reset, the default theme and theme-cyan have zero specificity apart from their own class, so your styles apply without !important.
  • <o-arc> and <o-progress> are placed with rotate instead of transform, and endings scale with the band. interactive makes the whole band clickable.
  • variant="stroke" on <o-progress> draws a 2px line over a gray track by default.

The 1.x source and README stay on GitHub, at the v1.6.0 tag.

Demos

From a checkout:

npm ci
npm run docs

Then open http://127.0.0.1:5174 (PORT=5175 npm run docs for another port). That is this repo's documentation site, from docs/, running on your local build. Start with the universe model: drag the corner of the box and watch the system follow. All the examples are in the gallery, from full pages to small pieces with their code open.

The documentation site has many more: gauges, charts, radial menus, knobs, watch faces. Orbit Kit has ready-made components built on Orbit.

Help and contributing

Report bugs in Issues, share what you made or ask in Discussions, or say hi in the Telegram group. To work on Orbit itself, see CONTRIBUTING.md.

For coding assistants, llms.txt is a short reference and llms-full.txt the complete one.

Sponsor

Orbit is free and made in spare time. If it saves you time, you can sponsor it on GitHub.

License

MIT

About

πŸ’« HTML on circles. A CSS layout system for radial UIs: gauges, dials, knobs, radial menus, charts and watch faces.

Topics

Resources

Code of conduct

Contributing

Stars

768 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages