@zumer/orbit
Version:
Orbit builds radial interfaces with CSS geometry and a small JavaScript layout runtime
325 lines (232 loc) • 18 kB
Markdown
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="orbit-logo-blanco.svg">
<img src="orbit-logo-negro.svg" alt="Orbit" width="120">
</picture>
</p>
# 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](https://zumerlab.com/orbit/) · [npm](https://www.npmjs.com/package/@zumer/orbit) · [Orbit 1.x docs](https://zumerlab.com/orbit-docs/)
## Install
```sh
npm install @zumer/orbit
```
```js
import '@zumer/orbit/style'
import '@zumer/orbit'
```
Or, in a plain page:
```html
<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.
```html
<!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`:
```html
<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:
```html
<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:
```html
<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:
```css
.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.
```html
<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:
```html
<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:
```html
<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.
```html
<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](https://zumerlab.com/orbit/demos/11-depth-layers.html) 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:
```js
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:
```ts
import type {} from '@zumer/orbit/react'
```
`@zumer/orbit/custom-elements.json` is a [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/). For attribute completion in VS Code:
```json
{ "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 documentation](https://zumerlab.com/orbit-docs/) stays online.
## Demos
From a checkout:
```sh
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](docs/demos/00-universe.html): 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](https://zumerlab.com/orbit/) has many more: [gauges](https://zumerlab.com/orbit/examples/gauges/), [charts](https://zumerlab.com/orbit/examples/charts/), [radial menus](https://zumerlab.com/orbit/examples/piemenu.html), [knobs](https://zumerlab.com/orbit/examples/knobs/), [watch faces](https://zumerlab.com/orbit/examples/watches/). [Orbit Kit](https://zumerlab.com/orbit/kit/) has ready-made components built on Orbit.
## Help and contributing
Report bugs in [Issues](https://github.com/zumerlab/orbit/issues), share what you made or ask in [Discussions](https://github.com/zumerlab/orbit/discussions), or say hi in the [Telegram group](https://t.me/ZumlyCommunity). To work on Orbit itself, see [CONTRIBUTING.md](CONTRIBUTING.md).
For coding assistants, [llms.txt](https://zumerlab.com/orbit/llms.txt) is a short reference and [llms-full.txt](https://zumerlab.com/orbit/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](https://github.com/sponsors/tinchox5).
## License
[MIT](LICENSE)