# Orbit documentation, full text
> Every page of https://zumerlab.com/orbit/ as plain text, with the code of its live examples. Generated by scripts/llms.mjs.
---
Source: https://zumerlab.com/orbit/
# HTML on circles.
Orbit is a CSS layout system for things that grow around a center: gauges, dials, knobs, radial menus, donut charts, watch faces. You write plain HTML with a few classes. Orbit does the trigonometry.
[How Orbit thinks](https://zumerlab.com/orbit/universe.html)
[See examples](https://zumerlab.com/orbit/examples/index.html)
[Star on GitHub](https://github.com/zumerlab/orbit)
`npm install @zumer/orbit`
## A small solar system
Orbit borrows its words from the sky. It sounds like a gimmick for about a minute. After that it is the easiest way to remember what each piece does.
- `.bigbang` The universe. Its box is the only size you set.
- `.gravity-spot` The center. Its gravity reaches the edge of the universe.
- `.orbit-1` … `.orbit-12` Twelve lanes from the center out to that edge.
- `.satellite` Travels a lane. Orbit spreads them around it and keeps your content upright.
```html
72%uploaded
```
```css
body {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
height: 100vh;
padding: 20px 16px 14px;
}
.bigbang { max-height: 100%; }
.controls { justify-content: center; }
/* cqmin: a share of the satellite, so the number scales with the ring. */
.readout strong { font-size: 22cqmin; }
o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
```
```js
const ring = document.querySelector('o-progress');
document.querySelector('input').addEventListener('input', (e) => {
ring.setAttribute('value', e.target.value);
ring.setAttribute('aria-valuenow', e.target.value);
document.querySelector('strong').textContent = e.target.value + '%';
});
```
A whole Orbit: a progress ring and a label. Nothing in it has a size in pixels, so the same markup works as a badge or a full-screen dial. Move the slider, or open the code and change it.
## Start somewhere
- [How Orbit thinks](https://zumerlab.com/orbit/universe.html): The model in one page, with live examples. Read this first.
- [Build a knob](https://zumerlab.com/orbit/guide.html): From an empty box to a control you can drag, step by step.
- [Examples](https://zumerlab.com/orbit/examples/index.html): Full pages with live controls, and small layouts to copy: gauges, a radar, watches, depth in 3D.
- [Orbit Kit](https://zumerlab.com/orbit/kit/index.html): Ready-made gauges, donuts, knobs and clocks, one tag each.
- [Coming from 1.x](https://zumerlab.com/orbit/migrating.html): What changed in 2.0, and one line to keep everything as it was.
Orbit is open source under the MIT license, made by [Zumerlab](https://github.com/zumerlab). If it saves you time, you can [sponsor its development](https://github.com/sponsors/tinchox5). Using Orbit 1.x? [Coming from 1.x](https://zumerlab.com/orbit/migrating.html) shows what changed.
---
Source: https://zumerlab.com/orbit/universe.html
# How Orbit thinks
Every Orbit layout is a tiny universe. Once the four words in this page click, the rest of Orbit is details.
## The universe
A `.bigbang` is the universe: the space your layout lives in. It is square by default, as wide as its parent, and a parent with a height keeps it from growing taller. Its box is the only size you ever set.
Inside it, a `.gravity-spot` is the center. Its gravity reaches as far as the shorter side of the universe, and that reach is split into 12 lanes. `orbit-12` touches the edge, `orbit-6` is halfway, `orbit-1` hugs the center.
```html
orbit-6
orbit-12
```
```css
/* The universe, tinted so you can see it. */
.bigbang {
background: color-mix(in srgb, var(--accent) 6%, var(--panel));
}
/* Rings are invisible; these borders only draw the lanes. */
[class*="orbit-"] {
border: 1px dashed color-mix(in srgb, var(--faint) 70%, transparent);
}
.half, .edge {
border: 1.5px solid var(--accent);
}
.tag {
padding: 2px 7px;
border-radius: 6px;
background: var(--bg);
color: var(--accent);
font: 12px var(--mono);
white-space: nowrap;
}
```
The light area is the universe. The preview is wider than it is tall, so the shorter side, 300px, sets the reach. There is also `orbit-0`, the center itself, and `orbit-13` to `orbit-24` past the edge, for things that should stick out.
## Things on the lanes
Rings hold things. A `.satellite` carries your content in a `.capsule`; Orbit spreads all the satellites of a ring evenly around it, and the capsule keeps the content upright wherever it ends up.
Every element on a ring is **one lane thick**. A satellite on `orbit-8` is a circle one lane across. To make things bigger or smaller, set `--o-size-ratio`, counted in lanes, on the element, on its ring or on the whole `.gravity-spot`:
```html
1
1
1
2
2
2
2
3
3
3
3
3
```
```css
[class*="orbit-"] {
border: 1px dashed var(--faint);
}
.satellite {
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 3px var(--bg);
color: var(--bg);
font: 600 12px/1 var(--mono);
}
/* The number is the thickness, in lanes. */
.orbit-7 .satellite { font-size: 14px; }
.orbit-10 .satellite { font-size: 18px; }
```
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.
## One box, any size
Because every ring, satellite and band is a fraction of the universe, a layout scales as one piece. You do not write breakpoints for it, and you do not call anything. Drag the corner of the dotted box:
```html
64
```
```css
/* Drag the corner: the system follows the box. */
.box {
width: 220px;
height: 220px;
margin: 20px;
resize: both;
overflow: hidden;
border: 1px dashed var(--faint);
border-radius: 10px;
}
o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
.orbit-6 .satellite {
border-radius: 50%;
background: var(--amber);
}
```
If you need a fixed size instead, give the reach a length on the `.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. Every `.gravity-spot` starts from the defaults, so the inner system does not inherit sizes or angles from the outer one.
```html
```
```css
.satellite { border-radius: 50%; }
.sun {
background: var(--amber);
box-shadow: 0 0 24px color-mix(in srgb, var(--amber) 60%, transparent);
}
.path { border: 1px dashed var(--line); }
.moons { border: 1px dashed var(--faint); }
.planet { background: var(--accent); }
.moon { background: var(--muted); }
/* Turning a ring turns everything on it, inner systems included. */
.path { animation: turn 40s linear infinite; }
.moons { animation: turn 6s linear infinite; }
@keyframes turn { to { rotate: 1turn; } }
```
Each planet is a satellite of the sun's `orbit-8`, five lanes thick. Inside it, a whole new system: the planet in its center and its moons on its own `orbit-10`, measured against the planet's satellite, not against the page.
## 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. A speedometer is a ring that uses 240° and starts at 240°, so its gap sits at the bottom:
```html
240°from 240°
```
```css
/* The full circle, for reference: the unused 120° is the gap. */
.whole { border: 1px dashed var(--line); }
o-arc { --o-gap: 3; --o-stroke: none; }
.ok { --o-fill: var(--accent); }
.warn { --o-fill: var(--amber); }
.bad { --o-fill: var(--coral); }
.vector { height: 2px; background: var(--faint); }
.readout strong { font-size: 30px; }
```
Without `value`, sibling arcs share the ring equally. `fit-range` puts the first and last tick on the two ends of the range instead of leaving a step at the end. Classes like `range-240`, `from-240` and `ccw` cover the usual values.
## Which way is out
Each satellite also knows where it sits: `--o-dir-x` and `--o-dir-y` are its direction from the center, from `-1` to `1` (`y` grows down). Use them to hang labels outward, here above the satellites of the upper half and below the rest:
```html
Mercury
Venus
Earth
Mars
Jupiter
Saturn
```
```css
.orbit-8 { border: 1px dashed var(--line); }
.capsule { aspect-ratio: 1; }
.dot {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
background: var(--accent);
}
.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%);
font-size: 13px;
white-space: nowrap;
}
.controls {
position: fixed;
left: 16px;
bottom: 14px;
}
```
```js
// Move the ring's start angle: labels switch sides as they cross the middle.
const ring = document.querySelector('.orbit-8');
const input = document.querySelector('input');
input.addEventListener('input', () => {
ring.style.setProperty('--o-from', input.value + 'deg');
document.querySelector('output').value = input.value + '°';
});
```
## The whole map
```
.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
│ └── ·
└── .orbit-M …
```
- Rings are direct children of a `.gravity-spot`, never of another ring.
- A ring holds only `.satellite`, `.vector`, `.side`, `` and ``.
- 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. More in [Visual aids](https://zumerlab.com/orbit/craft/visual-aids.html).
---
Source: https://zumerlab.com/orbit/install.html
# Install
Orbit is two files: a stylesheet and a small runtime. Load both. The CSS does the geometry and the look; the runtime registers `` and ``, numbers the rings and keeps everything drawn as your layout changes.
## From npm
```sh
npm install @zumer/orbit
```
Then, in the entry file of your app:
```js
import '@zumer/orbit/style';
import '@zumer/orbit';
```
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.
## From a CDN
Load it from unpkg. This always gets the latest release; add a version after the name (`@zumer/orbit@2`) if you want it to change only when you decide.
```html
```
The script puts the API on `window.Orbit`. To load it as an ES module instead: `import { Orbit } from 'https://unpkg.com/@zumer/orbit/dist/orbit.mjs'`.
## What is in the package
Everything lives in `dist/`. You can [browse it on unpkg](https://unpkg.com/browse/@zumer/orbit/dist/).
| File | Import as | What it is |
| `orbit.css`, `orbit.min.css` | `@zumer/orbit/style` | The stylesheet: geometry, reset, default theme, `theme-cyan`, the color palette and `dev-orbit`. About 18 kB gzipped. |
| `orbit.mjs` | `@zumer/orbit` (import) | The runtime as an ES module. |
| `orbit.js`, `orbit.min.js` | `@zumer/orbit` (require) | The runtime as a classic script that sets `window.Orbit`. About 9 kB gzipped. |
| `orbit.d.ts`, `style.d.ts` | | TypeScript types for the API, the elements and the stylesheet import. |
| `react.d.ts` | `@zumer/orbit/react` | JSX types for `` and ``. Types only, no code. See [React](https://zumerlab.com/orbit/frameworks/react.html). |
| `custom-elements.json` | `@zumer/orbit/custom-elements.json` | A [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/) for tools that read one. |
| `vscode.html-custom-data.json` | `@zumer/orbit/vscode.html-custom-data.json` | Attribute completion for VS Code. |
To get that completion, add this to `.vscode/settings.json`:
```json
{ "html.customData": ["./node_modules/@zumer/orbit/dist/vscode.html-custom-data.json"] }
```
## Try it
A layout is a `.bigbang` with a `.gravity-spot` inside, and rings inside that. Put things on the rings: satellites with your content, arcs, progress, ticks. This one is a ring of eight satellites around a label. Edit it here, or press *Copy page* and save it as an `index.html`: it loads Orbit from unpkg and needs nothing else.
```html
Orbit
1
2
3
4
5
6
7
8
```
```css
.orbit-8 {
border: 1px dashed var(--faint);
}
.orbit-8 .satellite {
border-radius: 50%;
background: color-mix(in srgb, var(--accent) 16%, var(--panel));
box-shadow: inset 0 0 0 1.5px var(--accent);
color: var(--accent);
font: 600 15px var(--mono);
transition: background .2s, color .2s;
}
.orbit-8 .satellite:hover {
background: var(--accent);
color: var(--bg);
}
.label {
font-size: 24px;
font-weight: 650;
letter-spacing: -.02em;
}
```
## When something looks wrong
Orbit explains common mistakes in the browser console, in messages that start with `[orbit]`. Messages about structure appear when `dev-orbit` is on the `.bigbang`; the others always appear. [Visual aids](https://zumerlab.com/orbit/craft/visual-aids.html) shows what `dev-orbit` draws.
| 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 console says *Orbit.resize() has nothing to do here* | The system already scales with its `.bigbang`. | Remove the call. See [Runtime and resizing](https://zumerlab.com/orbit/craft/runtime.html). |
## Browser support
Orbit needs CSS `sin()`, `cos()` and container query units, which current versions of Chrome, Edge, Firefox and Safari all have. In a browser without trigonometric functions, each `.bigbang` turns into a red box that asks for an update.
Without JavaScript, a CSS fallback still places simple layouts: up to 24 rings and 60 children of each kind per ring. The fallback also needs `:has()` and `:nth-child(… of …)`. `` and `` always need the runtime.
## Next
- [How Orbit thinks](https://zumerlab.com/orbit/universe.html) explains the model in one page.
- [Build a knob](https://zumerlab.com/orbit/guide.html) puts it to work step by step.
- With a framework: [React](https://zumerlab.com/orbit/frameworks/react.html), [Vue](https://zumerlab.com/orbit/frameworks/vue.html), [Svelte](https://zumerlab.com/orbit/frameworks/svelte.html), [Angular](https://zumerlab.com/orbit/frameworks/angular.html), [Astro](https://zumerlab.com/orbit/frameworks/astro.html).
---
Source: https://zumerlab.com/orbit/guide.html
# Build a knob
A volume knob: a ring of ticks, a band that shows the level, a body with a dot that turns, and the number under it. On the way you meet every piece of Orbit. Each step below is live: open its code and change it.
## 1. Make a universe
Everything starts with a [`.bigbang`](https://zumerlab.com/orbit/reference/bigbang.html). It is the space the knob lives in, and its box is the only size you set: the knob will be as big as this box. Here it is 260px wide and square by default. It is invisible; the dashed outline is only there so you can see it.
```html
```
```css
body {
display: grid;
place-items: center;
}
/* The only size you set. */
.knob {
width: 260px;
outline: 1px dashed var(--faint);
}
```
## 2. Give it a center
A [`.gravity-spot`](https://zumerlab.com/orbit/reference/gravity-spot.html) is the center of the system. Its gravity reaches the shorter side of the universe, and that reach is split into 12 lanes: the rings you will place things on. It is invisible too. The circle drawn here is its reach, `--o-force`.
```html
```
```css
body {
display: grid;
place-items: center;
}
.knob {
width: 260px;
outline: 1px dashed var(--faint);
}
/* Only to see it: the reach of the gravity spot. */
.gravity-spot::before {
content: '';
position: absolute;
left: 0;
top: 0;
translate: -50% -50%;
width: var(--o-force);
aspect-ratio: 1;
border-radius: 50%;
border: 1px dashed var(--accent);
}
```
## 3. Draw the orbits
[Rings](https://zumerlab.com/orbit/reference/orbit.html) go straight inside the gravity spot. `orbit-N` sits N lanes from the center: `orbit-6` halfway, `orbit-12` at the edge. The knob needs three: `orbit-11` for the ticks, `orbit-10` for the band, and `orbit-0`, the center itself, for the body.
Rings are transparent. The dashed lines here come from a `border` in the example CSS.
```html
```
```css
body {
display: grid;
place-items: center;
}
.knob {
width: 260px;
}
/* Only to see them. */
.orbit-11, .orbit-10 {
border: 1px dashed var(--accent);
}
```
`orbit-11` outside, `orbit-10` just inside it. `orbit-0` is the center point, with no size of its own.
## 4. Put things on them
Rings hold [satellites](https://zumerlab.com/orbit/reference/satellite.html) carrying your content, [ticks](https://zumerlab.com/orbit/reference/vector.html), [chords](https://zumerlab.com/orbit/reference/side.html), [arcs](https://zumerlab.com/orbit/reference/arc.html) and [progress bands](https://zumerlab.com/orbit/reference/progress.html), and Orbit spreads them evenly around their ring. Here: 21 ticks on `orbit-11`, a progress band on `orbit-10` and the number in the middle. The ticks are the same element 21 times, so one line of JavaScript writes them.
Centered content goes in `.orbit-0 > .satellite.at-center > .capsule`. Every element on a ring is one lane thick, so the center satellite gets `--o-size-ratio: 6` to make room for the number.
```html
64volume
```
```css
body {
display: grid;
place-items: center;
}
.knob {
width: 260px;
}
.orbit-11, .orbit-10 {
border: 1px dashed var(--faint);
}
.ticks .vector {
background: var(--muted);
}
o-progress {
--o-fill: var(--accent);
--o-stroke: none;
}
.readout strong {
font-size: 26px;
}
```
```js
document.querySelector('.ticks').innerHTML = ''.repeat(21);
```
## 5. Shape it
Now the details. The ticks and the band use 270° of the circle starting at 225° (`range-270 from-225`), so the knob has a gap at the bottom, and `fit-range` puts the first and last tick on the two ends of the range. `--o-size-ratio` makes the ticks a little shorter and the band a little thinner than a lane, and `shape="rounded"` rounds the band's ends. The track behind the band is `--o-back-fill`.
The number moves into the gap: a satellite on `orbit-11` with `angle-180` sits straight down. The dashed guides are gone.
```html
64volume
```
```css
body {
display: grid;
place-items: center;
}
.knob {
width: 260px;
}
.ticks .vector {
background: #3a4757;
}
o-progress {
--o-stroke: none;
--o-fill: var(--accent);
--o-back-fill: #1d2630;
}
.readout strong {
font-size: 22px;
}
```
```js
document.querySelector('.ticks').innerHTML = ''.repeat(21);
```
## 6. A system inside a satellite
A satellite is a universe too: put a `.gravity-spot` inside it and its rings are measured against the satellite. The body of the knob is a satellite 15 lanes wide on `orbit-0`. Its capsule paints the body, and the system inside it carries the dot on its own `orbit-9`, with the same range as the dial.
With one satellite on a partial range, the dot sits at the start, 225°. Turning that inner ring with `rotate` moves it along: 64% of 270° is about 173°.
```html
64volume
```
```css
body {
display: grid;
place-items: center;
}
.knob {
width: 260px;
}
.ticks .vector {
background: #3a4757;
}
o-progress {
--o-stroke: none;
--o-fill: var(--accent);
--o-back-fill: #1d2630;
}
.body {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle at 50% 30%, #2b3542, #151b22 70%);
box-shadow: 0 14px 30px -10px #000, inset 0 1px 0 #3b4655, inset 0 -6px 14px #0a0d11;
}
.dot {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
}
/* 64% of the 270° range. */
.turn {
rotate: 173deg;
}
.readout strong {
font-size: 22px;
}
```
```js
document.querySelector('.ticks').innerHTML = ''.repeat(21);
```
## 7. Make it turn
The drawing is done; the rest is a few lines of JavaScript. One function sets the value: it changes `value` on the ``, turns the inner ring through a `--turn` custom property, lights the ticks up to the value and keeps `aria-valuenow` in sync. The pointer, the wheel and the arrow keys all call it.
The `.bigbang` is the control: `role="slider"`, a name and `tabindex="0"`, so it works with a keyboard and a screen reader. Orbit draws; the behavior is yours.
```html
64volume
```
```css
body {
display: grid;
place-items: center;
}
.knob {
width: 300px;
touch-action: none;
cursor: grab;
}
.knob:active { cursor: grabbing; }
.knob:focus-visible { outline: none; }
.ticks .vector { background: #3a4757; }
.ticks .vector.lit { background: var(--accent); }
o-progress {
--o-stroke: none;
--o-fill: var(--accent);
--o-back-fill: #1d2630;
}
.body {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle at 50% 30%, #2b3542, #151b22 70%);
box-shadow: 0 14px 30px -10px #000, inset 0 1px 0 #3b4655, inset 0 -6px 14px #0a0d11;
}
.knob:focus-visible .body { box-shadow: 0 0 0 3px var(--accent), 0 14px 30px -10px #000; }
.dot {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
}
.turn {
rotate: var(--turn, 0deg);
transition: rotate .08s linear;
}
.readout strong { font-size: 24px; }
```
```js
const knob = document.querySelector('.knob');
const ticksRing = knob.querySelector('.ticks');
ticksRing.innerHTML = ''.repeat(21);
const ticks = [...ticksRing.children];
let value = 0;
const set = (v) => {
value = Math.max(0, Math.min(100, Math.round(v)));
knob.querySelector('o-progress').setAttribute('value', value);
knob.style.setProperty('--turn', value * 2.7 + 'deg');
knob.setAttribute('aria-valuenow', value);
document.getElementById('level').textContent = value;
ticks.forEach((tick, i) => tick.classList.toggle('lit', value > 0 && i * 5 <= value));
};
// Pointer angle around the center, 0 at the start of the 270° range.
const fromPointer = (event) => {
const box = knob.getBoundingClientRect();
const x = event.clientX - (box.left + box.width / 2);
const y = (box.top + box.height / 2) - event.clientY;
const along = (Math.atan2(x, y) * 180 / Math.PI + 360 + 135) % 360;
if (along <= 270) set(along / 2.7);
};
knob.addEventListener('pointerdown', (e) => { knob.setPointerCapture(e.pointerId); fromPointer(e); });
knob.addEventListener('pointermove', (e) => { if (knob.hasPointerCapture(e.pointerId)) fromPointer(e); });
knob.addEventListener('wheel', (e) => { e.preventDefault(); set(value - Math.sign(e.deltaY) * 2); }, { passive: false });
knob.addEventListener('keydown', (e) => {
const step = { ArrowUp: 1, ArrowRight: 1, ArrowDown: -1, ArrowLeft: -1, PageUp: 10, PageDown: -10 }[e.key];
if (step) { e.preventDefault(); set(value + step); }
if (e.key === 'Home') set(0);
if (e.key === 'End') set(100);
});
set(64);
```
Drag around the knob, scroll over it, or focus it and use the arrow keys.
Change `width` on `.knob` and all of it scales together, ticks, band, body and dot. Every element has more options like these: the pages under Elements list them. The same knob is also a [full-page demo](https://zumerlab.com/orbit/demos/05-knob.html), and [``](https://zumerlab.com/orbit/kit/knob.html) in Orbit Kit is a ready-made one.
---
Source: https://zumerlab.com/orbit/migrating.html
# Coming from 1.x
Orbit 2 changes one big thing: where size comes from. Everything else is smaller and mostly in your favor. You can move a layout over in a few minutes, or keep it exactly as it was with one line of CSS.
The 1.x source and README stay on GitHub, at the [v1.6.0 tag](https://github.com/zumerlab/orbit/tree/v1.6.0#readme).
## Size comes from the universe
In 1.x every system was 500px across, whatever the size of its `.bigbang`. `orbit-12` was always 500px wide, and to make a layout follow its container you called `Orbit.resize()`.
In 2.0 the `.bigbang` is the universe and its box sets the size. The `.gravity-spot` reaches the shorter side of it (`--o-force` defaults to `100cqmin`), so `orbit-12` touches the edge and every ring is a fraction of the box. Layouts follow their container with no code.
Both previews below are the same ring in a 300px-tall frame. The first keeps the 1.x size with a fixed reach; the second is written for 2.0.
```html
70%1.x size
```
```css
/* The reach of the system, drawn so you can compare. */
.reach {
border: 1px dashed var(--faint);
}
o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
.readout strong { font-size: 26px; }
```
`--o-force: 500px`: the dashed `orbit-12` is 500px across, larger than the frame, as in 1.x.
```html
70%2.0 size
```
```css
/* The reach of the system, drawn so you can compare. */
.reach {
border: 1px dashed var(--faint);
}
o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
.readout strong { font-size: 26px; }
```
No `--o-force`: the dashed `orbit-12` touches the edge of the frame, and `orbit-7` lands where 1.x put `orbit-4`.
## Two ways to move
**Keep the 1.x size.** Set the old reach back on your systems:
```css
.gravity-spot { --o-force: 500px; }
```
Your layouts look exactly as before. `Orbit.resize()` still scales systems with a fixed px reach, so code that used it keeps working.
**Adopt the new model.** Remove `--o-force` and `Orbit.resize()`, then pick rings by their share of the space. If your 1.x layout sat in a box `W` px wide:
- a ring `orbit-N` lands near `orbit-(N × 500 / W)`,
- a thickness ratio `r` lands near `--o-size-ratio: r × 500 / W`.
Round to the nearest lane; if that is visibly off, try the neighbor or nudge with `--o-alignment` in px. When every ring of a system scales the same way, set `--o-size-ratio` once on the `.gravity-spot`. Give auto-numbered `.orbit` rings an explicit number while you are at it, so the result does not depend on DOM order.
Text inside arcs keeps its font size, so on a smaller layout a label may no longer fit its arc. The console will tell you (`[orbit] text "…" overflows its arc`); widen the range, use `range="auto"`, or move the ring out a lane.
## Everything else
- **The universe.** A `.bigbang` is square by default and a size container, so it is the containing block of `position: fixed` descendants. Directly in `` it is capped at the viewport height instead of forced to it, so your own `width` and `aspect-ratio` apply.
- **Thickness.** `.satellite` no longer resets `--o-size-ratio`, so a value set on its ring reaches it. `shrink-N` on a ring only changes how close it sits to the previous lane; it does not shrink what is on it.
- **Nested systems.** Every `.gravity-spot` starts from the defaults, including ring spacing, so a `shrink-N` around a satellite does not leak into the system inside it.
- **Gaps and alignment.** `--o-gap` cascades from the ring and the system. `--o-aligment` still works; `--o-alignment` is the right spelling.
- **Ranges.** `fit-range` on a ring of arcs no longer makes the arcs overflow the range.
- **Styling.** Satellites have no border by default. The reset, the default theme and `theme-cyan` count only their own class for specificity, so a single class of yours wins without `!important`, also inside a `.capsule`. `dev-orbit` combines with a theme.
- **Arcs and progress.** They are placed with `rotate` instead of `transform`, so a `transform` of yours adds to it. Endings (`rounded`, `arrow`…) scale with the band. `interactive` makes the whole band clickable, transparent or not, and `hit-width` adds touch area. `variant="stroke"` draws a 2px line over a gray track by default.
- **New.** `range="auto"` and `text-anchor` for arcs that fit their text, `--o-dir-x` / `--o-dir-y` on satellites, depth layers, `Orbit.ready()`, the `o-rendered` event, `value` and `max` as properties on arcs and progress (so `[value]` bindings redraw), and `[orbit]` messages in the console.
- **No `!important`.** Utility classes (`grow-*x`, `shrink-N`, `angle-N`, `ccw`, `fit-range`, `at-*`, `flip`…) no longer use it. An inline style or a more specific rule of yours wins over them. In 1.x some of them could only be beaten with another `!important`; those still work, but you can drop them.
- **Renamed.** `from-1x` … `from-12x` are now `core-1` … `core-12`, so they don't read like `from-N`. The old names still work.
- **Fixed.** `angle-N` on a `.side` points where it does on a satellite. Only real `angle-N` classes reset `--o-from`, not any class that contains "angle-". `--o-color` set on a ring or satellite gives it its own `--o-color-*` shades.
---
Source: https://zumerlab.com/orbit/reference/bigbang.html
# Bigbang
Every Orbit layout starts with a big bang. The `.bigbang` creates the universe your system lives in, and its box is the only size you set: every ring, satellite and band inside is a fraction of it.
```html
orbit-12
orbit-6
```
```css
/* The universe box, tinted so you can see it. */
.bigbang {
background: color-mix(in srgb, var(--accent) 7%, transparent);
outline: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
outline-offset: -1px;
}
.edge { border-color: var(--accent); }
.half { border-color: var(--faint); }
.orbit-9 { border-color: var(--line); }
.label {
white-space: nowrap;
padding: 1px 6px;
border-radius: 6px;
background: var(--panel);
font: 12px var(--mono);
color: var(--muted);
}
.sun { background: var(--amber); box-shadow: 0 0 18px var(--amber); }
.planet { background: var(--text); }
.turn { animation: turn 40s linear infinite; }
.turn.fast { animation-duration: 16s; }
@keyframes turn { to { rotate: 360deg; } }
```
The tinted box is the universe. `orbit-12` touches its top and bottom, and `orbit-6` is halfway.
## How big it is
A `.bigbang` is as wide as its parent and square by default. A parent with a definite height caps it, so in a wide, short container it becomes a wide, short box, and the system inside takes the shorter side.
- To size it, give the `.bigbang` or its parent a width. Your own `width`, `height` or `aspect-ratio` always win, because the defaults have zero specificity.
- Directly inside `` it is capped at the viewport height. The previews on this site are pages like that, which is why the universe above is as tall as the frame.
- It is a size container: the `.gravity-spot` inside measures it with container query units. It is also the containing block of `position: fixed` descendants.
- A `.bigbang` with no width or height draws nothing, and the console says so with an `[orbit]` message.
Drag the corner of the dotted box. The system follows with no JavaScript: make the box wide and short and it takes the height, tall and narrow and it takes the width.
```html
```
```css
.box {
position: relative;
width: 320px;
height: 180px;
min-width: 60px;
min-height: 60px;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
resize: both;
overflow: hidden;
border: 2px dotted var(--faint);
border-radius: 4px;
}
.bigbang { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.orbit-12 { border-color: var(--accent); }
.orbit-9 { border-color: var(--line); }
.satellite { background: var(--accent); }
.size {
position: absolute;
left: 8px;
bottom: 6px;
font: 12px var(--mono);
color: var(--muted);
font-variant-numeric: tabular-nums;
}
```
```js
document.getElementById('dots').innerHTML = ''.repeat(8);
// Only to show the size of the box: the layout itself needs no script.
const box = document.querySelector('.box');
new ResizeObserver(() => {
document.getElementById('size').value = `${box.clientWidth} × ${box.clientHeight}`;
}).observe(box);
```
## Moving the center
The universe centers its system. An alignment class on the `.bigbang` puts the center on an edge or a corner of the box instead, so only part of the system shows. It is the quick way to a quarter dial in a corner or a half gauge along an edge.
```html
```
```css
.grid {
display: grid;
grid-template-columns: repeat(3, 84px);
justify-items: center;
gap: 12px 44px;
}
.cell { display: grid; justify-items: center; gap: 6px; }
.cell span { font: 12px var(--mono); color: var(--muted); white-space: nowrap; }
.bigbang {
width: 84px;
background: color-mix(in srgb, var(--accent) 7%, transparent);
border-radius: 4px;
overflow: hidden;
}
.orbit-6, .orbit-12 { border-color: var(--accent); }
/* The center of the system. */
.center { background: var(--amber); }
```
```js
const places = ['at-top-left', 'at-top', 'at-top-right', 'at-center-left', 'at-center',
'at-center-right', 'at-bottom-left', 'at-bottom', 'at-bottom-right'];
// The same system nine times; only the class on the .bigbang changes.
document.getElementById('grid').innerHTML = places.map(place => `
${place}
`).join('');
```
The amber dot is the center of the system. `.at-center` is the default.
## A universe inside a system
A `.bigbang` placed directly in a `.gravity-spot` takes the reach of that system instead of measuring its own box. That is how a whole system becomes one layer of a [depth stack](https://zumerlab.com/orbit/craft/depth.html).
## Reference
| Class or property | Default | What it does |
| `.bigbang` | | Creates the universe. Holds one or more `.gravity-spot`. |
| `width` / `height` / `aspect-ratio` | `100%` / `auto` / `1` | The size of the universe. Capped by a parent with a definite height, and by the viewport directly in ``. |
| `.at-top-left` … `.at-bottom-right` | `.at-center` | Puts the center of the system on an edge or a corner of the box. |
| `.dev-orbit` | | Draws the lanes and explains misplaced elements in the console. See [Visual aids](https://zumerlab.com/orbit/craft/visual-aids.html). |
---
Source: https://zumerlab.com/orbit/reference/gravity-spot.html
# Gravity spot
The `.gravity-spot` is the center of mass of a system. Every ring is its direct child, and its gravity reaches as far as the shorter side of the universe.
```html
```
```css
.stage { gap: 12px; align-content: center; }
/* A wide universe, tinted so you can see its box. */
.stage > .bigbang {
width: 100%;
height: calc(100vh - 84px);
aspect-ratio: auto;
background: color-mix(in srgb, var(--accent) 6%, transparent);
border-radius: 6px;
}
.gravity-spot > [class*="orbit-"] { border-color: var(--accent); }
.gravity-spot > .orbit-9, .gravity-spot > .orbit-6 { border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.satellite { background: var(--accent); }
.fixed > [class*="orbit-"] { border: 1px dashed var(--amber); }
.fixed .satellite { background: var(--amber); }
```
```js
const force = document.getElementById('force');
force.addEventListener('input', () => {
document.getElementById('fixed').style.setProperty('--o-force', force.value + 'px');
document.getElementById('out').value = force.value + 'px';
});
```
Two systems in one universe. The teal one uses the default reach and touches the top and bottom of the box; the amber one has `--o-force` in px and ignores the box.
## Reach
The reach is `--o-force`, the diameter of `orbit-12`. Between the center and that edge there are 12 lanes. The default, `100cqmin`, is the shorter side of the nearest `.bigbang` or satellite, so the system follows its box as it resizes. Set it when you want something else:
- a length, for a fixed size that ignores the box: `--o-force: 120px`;
- `100cqw`, to reach the width of a wide universe instead of its height;
- a fraction of the default, like `calc(100cqmin * .8)`, to leave a margin around the system.
Systems with a fixed px reach can still scale with their container through `Orbit.resize()`. See [Runtime and resizing](https://zumerlab.com/orbit/craft/runtime.html).
## Every system starts fresh
A `.gravity-spot` resets the reach, the angles, the thickness and the spacing to their defaults. A system nested in a satellite does not inherit any of them from the one around it.
A satellite is a universe too: a `.gravity-spot` inside one reaches the satellite's edge, so its `orbit-12` is the satellite's outline. Here the outer system makes its planets five lanes thick, and each planet holds a system of its own whose moons start again from one lane and set their own size.
```html
```
```css
.gravity-spot > .orbit-8 { border-color: var(--line); }
.sun { background: var(--amber); box-shadow: 0 0 24px color-mix(in srgb, var(--amber) 60%, transparent); }
/* The planet's outline is the orbit-12 of the system inside it. */
.planet {
background: color-mix(in srgb, var(--accent) 10%, var(--bg));
border: 1px solid var(--accent);
}
.moons {
border: 1px dashed color-mix(in srgb, var(--accent) 45%, transparent);
animation: turn 12s linear infinite;
}
.moon { background: var(--accent); }
@keyframes turn { to { rotate: 360deg; } }
```
Each planet holds a system of its own. Its moons are three lanes of the planet, not of the sun.
## An empty core
`.core-1` to `.core-12` on a `.gravity-spot` leave an empty core in the middle, N lanes wide. With `core-4` the radius is split into 16 lanes instead of 12 and `orbit-N` lands on lane N + 4: rings sit closer together, further from the center, and `orbit-12` still touches the edge. Compare the cyan rings (default) with the purple ones (`core-4`), both `orbit-4`, `orbit-8` and `orbit-12`.
In 1.x these classes were `from-1x` to `from-12x`. They still work.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
.gravity-spot > * { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
#cored > * { border: 1px dashed var(--amber); }
```
```js
const core = document.getElementById('core'), cored = document.getElementById('cored');
core.addEventListener('input', () => {
cored.className = 'gravity-spot' + (core.value > 0 ? ' core-' + core.value : '');
document.getElementById('out').value = core.value > 0 ? 'core-' + core.value : 'no core';
});
```
Teal: `orbit-4`, `orbit-8` and `orbit-12` with no core. Amber: the same rings in a system with an empty core.
## Rings off the center
A `.gravity-spot` centers its rings on itself. An alignment class on it (`.at-top-left`, `.at-top`, `.at-bottom-right` and the rest) moves every ring toward that edge or corner, so the rings touch the center instead of surrounding it. Four systems in the same spot, one per corner:
```html
```
```css
.gravity-spot[class*="at-"] > * {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.center { background: var(--amber); }
```
Every ring touches the center, the amber dot, instead of surrounding it.
## Reference
Set these on the `.gravity-spot` to change the whole system, or on a ring or element for just that part.
| Class or property | Default | What it does |
| `--o-force` | `100cqmin` | Reach: the diameter of `orbit-12`. |
| `--o-size-ratio` | `1` | Thickness of the elements on the rings, in lanes. |
| `--o-range` | `360deg` | How much of the circle the rings use. See [Orbits](https://zumerlab.com/orbit/reference/orbit.html#angles). |
| `--o-from` | `0deg` | Where the range starts, clockwise from the top. |
| `--o-gap` | `1` | Space between sibling arcs. |
| `--o-ellipse-x` / `--o-ellipse-y` | `1` | Stretch the rings into ellipses. See [Orbits](https://zumerlab.com/orbit/reference/orbit.html#ellipses). |
| `.core-1` … `.core-12` | | An empty core N lanes wide; every ring moves N lanes out. Old name: `from-Nx`. |
| `.at-top-left` … `.at-bottom-right` | `.at-center` | Move the rings toward an edge or a corner, touching the center. |
| `.depth` | | Stacks the rings as 3D layers. See [Depth layers](https://zumerlab.com/orbit/craft/depth.html). |
---
Source: https://zumerlab.com/orbit/reference/orbit.html
# Orbits
Gravity draws orbits. A ring, `.orbit-N`, is a lane around the gravity spot, N twelfths of the way out to the edge, and it is where satellites, vectors, sides, arcs and progress travel.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
#lanes > * { border-color: var(--line); }
#lanes > .orbit-12 { border-color: var(--accent); }
/* The chosen lane, filled so you can see its diameter. */
#lanes > .on {
border: 2px solid var(--amber);
background: color-mix(in srgb, var(--amber) 10%, transparent);
}
.dot { background: var(--amber); }
```
```js
const lanes = document.getElementById('lanes'), lane = document.getElementById('lane');
// orbit-1 … orbit-12, and a dot on orbit-0, the center.
lanes.innerHTML = Array.from({ length: 12 }, (_, i) => ``).join('') +
'
';
const show = () => {
const n = Number(lane.value);
lanes.querySelectorAll(':scope > *').forEach(ring => ring.classList.toggle('on', ring.matches('.orbit-' + n) && n > 0));
document.getElementById('out').value = n ? `orbit-${n}: ${n}/12 of the reach` : 'orbit-0: the center';
};
lane.addEventListener('input', show);
show();
```
The 12 lanes. `orbit-6` is halfway and `orbit-12`, in teal, touches the edge of the universe.
## Which lane
- The diameter of `orbit-N` is N/12 of the reach (`--o-force`): `orbit-3` is a quarter, `orbit-6` half, `orbit-12` all of it.
- `orbit-0` is the center itself. It holds centered content; see The center.
- `orbit-13` to `orbit-24` lie past the edge, for things that should stick out. With the runtime, higher numbers work too.
- `.orbit` without a number takes the next lane in order: the first `.orbit` of a system is lane 1, the second lane 2. Numbered rings are not counted.
Rings are direct children of a `.gravity-spot`, never of another ring. A ring holds only `.satellite`, `.vector`, `.side`, `` and ``. With `dev-orbit` on the `.bigbang`, the console points out anything else.
## Look
Rings are invisible. Give them a border or a background to draw them.
```html
```
```css
/* A ring is a round box: borders, backgrounds and shadows draw it. */
.glow {
border: 2px solid var(--accent);
box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 50%, transparent),
inset 0 0 18px color-mix(in srgb, var(--accent) 30%, transparent);
}
.dotted {
border: 3px dotted var(--amber);
}
.filled {
border: 1px dashed var(--accent);
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
```
## Thickness
Everything on a ring is one lane thick: a twelfth of the radius. Set `--o-size-ratio`, in lanes, on the ring to change all its elements, on the `.gravity-spot` for the whole system, or on one element. 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.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
[class*="orbit-"] { border-color: var(--line); }
.satellite { background: var(--accent); }
#b .satellite { background: var(--amber); }
```
```js
const fill = (id, count) => document.getElementById(id).innerHTML = ''.repeat(count);
fill('a', 3); fill('b', 4); fill('c', 6);
const size = document.getElementById('size');
size.addEventListener('input', () => {
document.getElementById('b').style.setProperty('--o-size-ratio', size.value);
document.getElementById('out').value = size.value;
});
```
`orbit-4` at the default, `orbit-10` at half a lane, and `orbit-7` at the thickness you pick.
## Spacing
`.shrink-5` to `.shrink-100`, in steps of 5, move a ring toward the previous lane: `orbit-10 shrink-40` sits 40% of a lane inside lane 10. On a ring, `shrink-N` only changes where it sits; the elements on it keep their size.
```html
orbit-10
orbit-9
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: 100%; height: calc(100vh - 84px); aspect-ratio: auto; overflow: hidden; }
.lane { border-color: var(--faint); }
#ring { border: 2px solid var(--amber); }
#ring .satellite { background: var(--accent); }
.label {
white-space: nowrap;
padding: 1px 6px;
border-radius: 6px;
background: var(--panel);
font: 12px var(--mono);
color: var(--muted);
}
```
```js
const shrink = document.getElementById('shrink'), ring = document.getElementById('ring');
shrink.addEventListener('input', () => {
const n = Number(shrink.value);
ring.className = 'orbit-10 range-100 from-310 fit-range' + (n ? ' shrink-' + n : '');
document.getElementById('out').value = n ? 'shrink-' + n : 'no shrink';
});
```
The amber ring is `orbit-10` with `shrink-N`: at 100 it reaches `orbit-9`. Its satellites keep their size.
## Angles
A ring spreads its elements evenly. Five satellites on a full ring sit 72° apart, starting at the top and going clockwise. Each kind is spread on its own, so five satellites and twelve vectors can share a ring.
- `--o-range` is how much of the circle the ring uses (`360deg` by default). `.range-0` to `.range-360` set it in whole degrees.
- `--o-from` is where the range starts, clockwise from the top. `.from-0` to `.from-360` set it.
- `.fit-range` puts the first and last element on the two ends of a partial range, instead of leaving one step free at the end.
- `.ccw` goes counterclockwise.
All of them cascade, so you can set them on the `.gravity-spot` for every ring at once. To place one element at an exact angle, use `.angle-N` on it; see [Satellites](https://zumerlab.com/orbit/reference/satellite.html#angle).
```html
`).join('');
const $ = id => document.getElementById(id);
const set = () => {
const range = Number($('range').value), from = Number($('from').value), ccw = $('ccw').checked;
// The ring: what you would write in your markup.
ring.className = `orbit-9 range-${range} from-${from}` + ($('fit').checked ? ' fit-range' : '') + (ccw ? ' ccw' : '');
// The band always runs clockwise, so a ccw range starts `range` degrees earlier.
band.style.setProperty('--o-range', range + 'deg');
band.style.setProperty('--o-from', (ccw ? from - range : from) + 'deg');
$('classes').textContent = ring.className;
};
document.querySelectorAll('input').forEach(input => input.addEventListener('input', set));
set();
```
Five satellites and the range they share. Try `range-180` with and without `fit-range`.
## The center
`orbit-0` is the center of the system. Put a `.satellite.at-center` in it for centered content, and size it with `--o-size-ratio` on `orbit-0`: the value is its diameter in lanes, so `12` reaches `orbit-6` and `24` fills the universe.
```html
orbit-12
orbit-6
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
.guide { border: 1px dashed var(--faint); z-index: 1; }
.disc {
background: color-mix(in srgb, var(--accent) 22%, transparent);
border: 2px solid var(--accent);
}
.label {
white-space: nowrap;
padding: 1px 6px;
border-radius: 6px;
background: var(--panel);
font: 12px var(--mono);
color: var(--muted);
}
```
```js
const size = document.getElementById('size');
size.addEventListener('input', () => {
document.getElementById('center').style.setProperty('--o-size-ratio', size.value);
document.getElementById('out').value = size.value;
});
```
At 12 the disc reaches `orbit-6`; at 24 it fills the universe.
## Ellipses
`--o-ellipse-x` and `--o-ellipse-y` stretch a ring into an ellipse. Values below 1 stretch it along that axis: `--o-ellipse-x: .5` makes it twice as wide. Satellites and vectors follow the ellipse; arcs, progress and sides are drawn for circles, so leave them on round rings.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
.round { border: 1px dashed var(--faint); }
.stretched { border: 1px solid var(--accent); }
.satellite { background: var(--accent); }
```
```js
const ring = document.getElementById('ring');
ring.innerHTML = ''.repeat(8);
for (const axis of ['x', 'y']) {
const input = document.getElementById(axis);
input.addEventListener('input', () => {
ring.style.setProperty('--o-ellipse-' + axis, input.value);
document.getElementById(axis + '-out').value = Number(input.value);
});
}
```
Eight satellites on a stretched `orbit-6`, over the round one.
## Gooey effect
`.gooey-fx` (or `.gooey-fx-light`), `.gooey-fx-medium` and `.gooey-fx-max` blend touching elements of a ring into one blob with an SVG filter. Safari does not support it.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
.track { border-color: var(--line); }
.satellite { background: var(--accent); }
.blob { animation: turn 8s linear infinite; }
input[type=checkbox] { accent-color: var(--accent); }
@keyframes turn { to { rotate: 360deg; } }
```
```js
const ring = document.getElementById('ring'), size = document.getElementById('size');
size.addEventListener('input', () => {
ring.style.setProperty('--o-size-ratio', size.value);
document.getElementById('out').value = size.value;
});
document.getElementById('gooey').addEventListener('change', (event) => ring.classList.toggle('gooey-fx', event.target.checked));
```
## Stacking
Rings stack in document order: a later ring paints over an earlier one. To stack them in 3D, with perspective and animated depth changes, see [Depth layers](https://zumerlab.com/orbit/craft/depth.html).
## Reference
| Class or property | Default | What it does |
| `.orbit-0` … `.orbit-24` | | A ring on lane N. 0 is the center, 13 and up stick out. |
| `.orbit` | | A ring on the next lane, counted in document order. |
| `--o-size-ratio` | `1` | Thickness of the ring's elements, in lanes. |
| `--o-range`, `.range-N` | `360deg` | How much of the circle the ring uses. |
| `--o-from`, `.from-N` | `0deg` | Where the range starts, clockwise from the top. |
| `.fit-range` | | First and last element on the two ends of the range. |
| `.ccw` | | Counterclockwise. |
| `.shrink-5` … `.shrink-100` | | Moves the ring toward the previous lane, by that percentage of a lane. |
| `--o-ellipse-x` / `--o-ellipse-y` | `1` | Stretch the ring into an ellipse. |
| `.gooey-fx`, `-medium`, `-max` | | Blends touching elements together. |
---
Source: https://zumerlab.com/orbit/reference/satellite.html
# Satellites
Satellites travel the orbits. A `.satellite` sits on a ring and carries your content in a `.capsule`; Orbit spreads all the satellites of a ring evenly around it.
```html
`).join('');
document.getElementById('out').value = count.value;
};
count.addEventListener('input', fill);
fill();
```
A satellite is a circle one lane across, with no border or background until you give it one. Put content in a `.capsule` inside it, never directly in the satellite: the capsule keeps it centered and upright. See [Capsules](https://zumerlab.com/orbit/reference/capsule.html).
## Size
Set `--o-size-ratio`, in lanes, on the satellite, its ring or the `.gravity-spot`. For one satellite there are also classes, which win over any ratio set around them:
- `.shrink-5` to `.shrink-100` make it smaller than a lane: `shrink-30` is 0.7 lanes.
- `.grow-1x` to `.grow-12x` make it 2 to 24 lanes. Decimals add fifths of a lane: `grow-1.2x` is 2.4 lanes.
- `.grow-0.1x` to `.grow-0.9x` add tenths of a lane: `grow-0.5x` is 1.5 lanes.
```html
```
```css
.orbit-8 { border-color: var(--faint); }
.satellite { background: var(--accent); }
```
Clockwise from the top: half a lane, one lane, 1.5, 2, 3 and 4 lanes.
## Shape and look
Satellites are round. `.box` makes one square and `.rounded-box` a square with soft corners. Anything else is plain CSS: backgrounds, borders, images, SVG.
```html
```
```css
.orbit-8 { border-color: var(--faint); }
.satellite { background: var(--accent); }
/* Anything else is plain CSS. */
.sphere {
background: radial-gradient(circle at 35% 30%, #fff, var(--accent) 35%, color-mix(in oklab, var(--accent) 35%, black));
box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 45%, transparent);
}
.outline {
background: var(--bg);
border: 2px dashed var(--accent);
}
```
Clockwise from the top: round, `.box`, `.rounded-box`, a gradient and a border.
## Position on the lane
A satellite is centered on its lane. `.inner-orbit` moves it in so its outer edge touches the lane, and `.outer-orbit` moves it out so its inner edge does. `.quarter-inner-orbit` and `.quarter-outer-orbit` move it part of the way. For any other offset, set `--o-alignment`: a positive length moves it in, a negative one out.
```html
```
```css
/* The lane on top, so you can see where each satellite sits. */
.orbit-8 { border: 2px solid var(--amber); z-index: 1; }
.satellite {
background: color-mix(in srgb, var(--accent) 30%, transparent);
border: 1px solid var(--accent);
}
```
Clockwise from the top: `outer-orbit`, `quarter-outer-orbit`, default, `quarter-inner-orbit`, `inner-orbit`.
## An exact angle
`.angle-0` to `.angle-360` place a satellite at that angle, clockwise from the top, instead of its turn on the ring. It ignores the ring's `--o-from`.
```html
45°
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
.lane { border-color: var(--faint); }
.satellite { background: var(--accent); }
.capsule { color: var(--bg); font-size: 13px; font-weight: 650; font-variant-numeric: tabular-nums; }
.vector { height: 2px; background: var(--amber); }
```
```js
const angle = document.getElementById('angle');
angle.addEventListener('input', () => {
const n = angle.value;
document.getElementById('moon').className = `satellite angle-${n}`;
document.getElementById('hand').className = `vector angle-${n}`;
document.getElementById('label').textContent = n + '°';
document.getElementById('out').value = 'angle-' + n;
});
```
## Facing the center
A satellite keeps its content upright wherever it is. With `.spin-lock` it turns with its angle instead, so the top of its content always points at the center, like the labels on a wheel. Add `.flip` to the capsule to make it point outward. Left, the default; right, `spin-lock`.
```html
';
document.getElementById('upright').innerHTML = arrows('').repeat(6);
document.getElementById('locked').innerHTML = arrows('spin-lock').repeat(6);
```
Left, every arrow points up. Right, with `spin-lock`, every arrow points at the center.
## Which way is out
Each satellite exposes `--o-dir-x` and `--o-dir-y`: the direction from the center of its system to the satellite, from -1 to 1. `x` grows to the right and `y` grows down, so a satellite at the top has `0` and `-1`, and a `.satellite.at-center` has both at `0`. Its content inherits them, so CSS can push a label outward whatever the angle:
```html
`).join('');
const push = document.getElementById('push');
push.addEventListener('input', () => {
document.querySelector('.bigbang').style.setProperty('--push', push.value + 'px');
document.getElementById('out').value = push.value + 'px';
});
```
Each label moves away from the center along its own direction.
[How Orbit thinks](https://zumerlab.com/orbit/universe.html) shows the other common use: labels that hang above the satellites of the upper half and below the rest.
## A satellite is a universe
Put a `.gravity-spot` directly inside a satellite and it reaches the satellite's edge: a moon system around a planet, a gauge inside a menu item. Make the satellite thicker to give it room. See [Gravity spot](https://zumerlab.com/orbit/reference/gravity-spot.html).
## Reference
| Class or property | Default | What it does |
| `--o-size-ratio` | `1` | Diameter, in lanes. |
| `.shrink-5` … `.shrink-100` | | Smaller than a lane: `shrink-N` is 1 − N/100 lanes. |
| `.grow-1x` … `.grow-12x`, `.grow-0.1x` … | | Bigger: `grow-Nx` is 2N lanes. |
| `.box`, `.rounded-box`, `.circle` | round | Shape. |
| `.inner-orbit`, `.outer-orbit`, `.quarter-inner-orbit`, `.quarter-outer-orbit` | | Moves it in or out from its lane. |
| `--o-alignment` | `0px` | Any offset from the lane; positive is inward. |
| `.angle-0` … `.angle-360` | | An exact angle, clockwise from the top. |
| `.spin-lock` | | Turns with its angle instead of staying upright. |
| `.at-center` | | In `orbit-0`: the satellite sits at the center. |
| `.at-top-left` … `.at-bottom-right` | | Where the capsule sits inside the satellite. |
| `--o-dir-x` / `--o-dir-y` | read only | Direction from the center, from -1 to 1. |
---
Source: https://zumerlab.com/orbit/reference/capsule.html
# Capsules
A `.capsule` is the cargo a satellite carries: your content. It stays centered and upright however far around the ring the satellite travels.
```html
Text
72
```
```css
.orbit-8 { border-color: var(--faint); }
.satellite {
background: color-mix(in srgb, var(--accent) 10%, var(--bg));
border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
}
.capsule { color: var(--accent); font-size: 14px; }
.capsule button { padding: 4px 8px; }
.number strong { font-size: 18px; font-variant-numeric: tabular-nums; }
```
```js
const count = document.getElementById('count');
document.getElementById('add').addEventListener('click', () => {
count.textContent = (Number(count.textContent) + 1) % 100;
});
```
Text, an SVG icon, a button and a number, each in a capsule.
Any HTML goes in a capsule. It works inside a `.satellite` and inside a `.side`. Content placed directly in a satellite is not centered or kept upright, and with `dev-orbit` on the `.bigbang` the console says so.
A capsule is as wide as its satellite and as tall as its content, so text wraps at the satellite's width. Give the capsule its own `width` for a label wider than the satellite. Like satellites, capsules have no border or background until you give them one.
## Where it sits in the satellite
A capsule is centered in its satellite. An alignment class on the satellite moves it: `.at-top-left`, `.at-top`, `.at-top-right`, `.at-center-left`, `.at-center-right`, `.at-bottom-left`, `.at-bottom` or `.at-bottom-right`. `.at-center` is the exception: on a satellite it places the satellite itself at the center of the system.
## Turning the content
`.flip` turns a capsule upside down, `.turn-left` a quarter turn left and `.turn-right` a quarter turn right. In a `.spin-lock` satellite the content turns with the satellite and points its top at the center; `.flip` makes it point outward.
```html
Orbit
right
flip
left
out
out
out
```
```css
.orbit-9, .orbit-4 { border-color: var(--faint); }
.satellite {
background: color-mix(in srgb, var(--accent) 14%, var(--bg));
color: var(--accent);
font-size: 13px;
font-weight: 600;
}
.inner .satellite {
background: color-mix(in srgb, var(--amber) 14%, var(--bg));
color: var(--amber);
}
```
Outer ring: the four orientations. Inner ring: `spin-lock` satellites with flipped capsules, reading outward.
## In a side
In a `.side`, a capsule lies along the side and turns with it, which suits labels on the edges of a polygon. Labels on the lower edges read upright; `.flip` turns over the ones at the top, and `.horizontal` keeps a capsule level instead.
```html
flip
along
along
horizontal
along
flip
```
```css
.side { height: 2px; background: var(--faint); }
.capsule span {
padding: 2px 9px;
border-radius: 10px;
background: color-mix(in srgb, var(--accent) 16%, var(--bg));
color: var(--accent);
font-size: 12px;
white-space: nowrap;
}
```
## Reference
| Class | Goes on | What it does |
| `.capsule` | child of `.satellite` or `.side` | Holds content, centered and upright. As wide as its satellite. |
| `.flip` | capsule | Upside down. |
| `.turn-left` / `.turn-right` | capsule | A quarter turn. |
| `.horizontal` | capsule in a side | Stays level instead of following the side. |
| `.at-top-left` … `.at-bottom-right` | satellite | Moves the capsule inside the satellite. |
| `.spin-lock` | satellite | The capsule turns with the satellite. See [Satellites](https://zumerlab.com/orbit/reference/satellite.html). |
---
Source: https://zumerlab.com/orbit/reference/vector.html
# Vectors
A `.vector` is a ray pointing out from the center, laid across its ring. A ring of vectors is a scale; a long one is a needle.
```html
```
```css
.face {
background: radial-gradient(circle at 50% 35%, #1a222c, #0e1318 72%);
box-shadow: inset 0 0 0 1px var(--line);
}
.minutes .vector { background: var(--faint); }
.hours .vector { height: 3px; background: var(--text); }
/* A hand turns with its ring. */
.hand { rotate: var(--turn, 0deg); }
#hour .vector { height: 5px; border-radius: 3px; background: var(--text); }
#minute .vector { height: 3px; border-radius: 2px; background: var(--accent); }
#second .vector { height: 1px; background: var(--amber); }
.hub { background: var(--amber); }
```
```js
document.getElementById('minutes').innerHTML = ''.repeat(60);
document.getElementById('hours').innerHTML = ''.repeat(12);
const turn = (id, deg) => document.getElementById(id).style.setProperty('--turn', deg + 'deg');
const tick = () => {
const now = new Date(), s = now.getSeconds(), m = now.getMinutes() + s / 60, h = now.getHours() % 12 + m / 60;
turn('second', s * 6);
turn('minute', m * 6);
turn('hour', h * 30);
document.querySelector('.clock').setAttribute('aria-label', now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }));
};
tick();
setInterval(tick, 1000);
```
60 and 12 ticks share `orbit-11`. The three hands are vectors on `orbit-3`, `orbit-4` and `orbit-5`, each turned with `rotate`.
## Length and look
A vector is one lane long and 1px thick, in the text color. Its look is plain CSS: `height` for the thickness, `background` for the color, `border-radius` for round ends. For the length, set `--o-size-ratio` in lanes on the vector, its ring or the `.gravity-spot`, or use the classes:
- `.shrink-5` to `.shrink-100`: shorter than a lane. `shrink-30` is 0.7 lanes.
- `.grow-1x` to `.grow-12x`: 2 to 24 lanes, with decimals like `grow-1.5x` (3 lanes). `.grow-0.1x` to `.grow-0.9x` add tenths of a lane.
Like every element on a ring, a vector is centered on its lane and reaches half its length to each side. So a vector `2N` lanes long on `orbit-N` starts at the center: that is the minute hand above, 8 lanes on `orbit-4`.
```html
```
```css
.orbit-8 { border: 1px dashed var(--faint); }
.vector { height: 3px; border-radius: 2px; background: var(--accent); }
/* The look is plain CSS. */
.thick { --o-size-ratio: 2; height: 8px; border-radius: 4px; background: var(--amber); }
.dashed {
--o-size-ratio: 3;
border-radius: 0;
background: repeating-linear-gradient(90deg, var(--amber) 0 5px, transparent 5px 9px);
}
```
Clockwise from the top: half a lane, one, 1.5, 2, 3 and 4 lanes, then two styled with CSS.
## Position and angle
`.inner-orbit` moves a vector in so it ends on its lane, and `.outer-orbit` moves it out so it starts there; `.quarter-inner-orbit` and `.quarter-outer-orbit` go part of the way, and `--o-alignment` takes any length. `.angle-0` to `.angle-360` point a vector at an exact angle, clockwise from the top. The ring's range, start and direction work as for satellites; see [Orbits](https://zumerlab.com/orbit/reference/orbit.html#angles).
```html
```
```css
.lane { border: 1px dashed var(--faint); }
.lane .vector { height: 4px; border-radius: 2px; background: var(--accent); }
.ticks .vector { height: 2px; background: var(--amber); }
```
```js
document.getElementById('ticks').innerHTML = ''.repeat(7);
```
On `orbit-8`: `outer-orbit` at 0°, default at 90°, `inner-orbit` at 180°. On `orbit-11`: seven ticks over 90° from 225°.
## Reference
| Class or property | Default | What it does |
| `--o-size-ratio` | `1` | Length, in lanes. |
| `height`, `background` | `1px`, `currentcolor` | Thickness and color. |
| `.shrink-5` … `.shrink-100` | | Shorter: `shrink-N` is 1 − N/100 lanes. |
| `.grow-1x` … `.grow-12x`, `.grow-0.1x` … | | Longer: `grow-Nx` is 2N lanes. |
| `.inner-orbit`, `.outer-orbit`, `.quarter-inner-orbit`, `.quarter-outer-orbit` | | Moves it in or out from its lane. |
| `--o-alignment` | `1px` | Any offset from the lane; positive is inward. |
| `.angle-0` … `.angle-360` | | An exact angle, clockwise from the top. |
---
Source: https://zumerlab.com/orbit/reference/side.html
# Sides
A `.side` is a straight line between two neighbors on a ring. Put N sides on a ring and they close a regular polygon with N corners.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
.side { height: 2px; background: var(--accent); }
.mid .side { background: color-mix(in srgb, var(--accent) 60%, transparent); }
.inner .side { background: var(--amber); }
```
```js
const outer = document.getElementById('outer'), count = document.getElementById('count');
const draw = () => {
outer.innerHTML = ''.repeat(count.value);
document.getElementById('out').value = count.value;
};
count.addEventListener('input', draw);
draw();
```
Sides on `orbit-11`, four on `orbit-8` and three on `orbit-5`.
A side is 1px thick, in the text color. Its look is plain CSS: `height` for the thickness and `background` for the color. Its length comes from the number of sides on the ring, so it has no size classes.
## Inside or around the ring
By default the corners of the polygon sit on the ring. With `.outer-orbit` on every side, the middle of each side touches the ring instead, and the polygon wraps around it.
```html
```
```css
.ring { border: 1px dashed var(--faint); }
.side { height: 2px; background: var(--accent); }
.outer-orbit { background: var(--amber); }
```
Teal: corners on the ring. Amber: `.outer-orbit`, sides touching it.
## Partial polygons
Sides follow the ring's `--o-range` and `--o-from`: three sides on `range-180` are half a hexagon. To put a single side at a given angle, use `.angle-N` on it, as with satellites: `angle-90` is the right edge.
```html
```
```css
.orbit-10, .orbit-6 { border: 1px dashed var(--line); }
.side { height: 3px; background: var(--accent); }
.four .side { background: var(--amber); }
```
Half a hexagon on `orbit-10` (`range-180 from-270`), and four sides over 240° on `orbit-6`.
A side can carry a `.capsule` for a label along its edge. See [Capsules](https://zumerlab.com/orbit/reference/capsule.html).
## Reference
| Class or property | Default | What it does |
| `.side` | | One edge of a regular polygon, as long as the ring's range divided by the number of sides. |
| `height`, `background` | `1px`, `currentcolor` | Thickness and color. |
| `.outer-orbit` | | The side touches the ring at its middle, so the polygon wraps around it. |
| `--o-range`, `--o-from`, `.ccw` | | From the ring. See [Orbits](https://zumerlab.com/orbit/reference/orbit.html#angles). |
| `.angle-N` | | Puts the side at N degrees, clockwise from the top. |
---
Source: https://zumerlab.com/orbit/reference/arc.html
# Arcs
`` draws a piece of a lane, with text along it if you want. Sibling arcs split their ring between them, or stack one after another by value.
```html
Segments
```
```css
.label o-arc { --o-fill: transparent; color: var(--muted); font-size: 13px; }
.segments o-arc {
--o-fill: color-mix(in srgb, var(--accent) 22%, transparent);
--o-gap: 3;
cursor: pointer;
}
.segments o-arc:hover { --o-fill: var(--accent); }
.donut o-arc { --o-stroke: none; }
.donut o-arc:nth-child(1) { --o-fill: var(--accent); }
.donut o-arc:nth-child(2) { --o-fill: color-mix(in srgb, var(--accent) 55%, var(--bg)); }
.donut o-arc:nth-child(3) { --o-fill: color-mix(in srgb, var(--accent) 25%, var(--bg)); }
```
```js
document.getElementById('segments').innerHTML = ''.repeat(8);
```
A label that fits its text, eight equal segments (hover them), and a donut of 45, 30 and 25.
Arcs go straight into a ring, next to satellites or vectors if you like. They are drawn by the runtime, so they need the Orbit script.
## Reference
| Attribute | Default | What it does |
| `value` | | Share of the ring's range, out of `max`. Without it, arcs split the range equally. See Split or stack. |
| `max` | `100` | The value that fills the whole range. |
| `range` | | `auto` makes the arc as long as its text. See Arcs that fit their text. |
| `shape` | `none` | The ending. See Endings. |
| `text-anchor` | `middle` | Text along the arc: `start`, `middle` or `end`. On a `range="auto"` arc, where the arc sits around its angle. |
| `flip` | | Runs the text the other way, so it reads upright at the bottom. Also works as a class. |
| `fit-range` | | Stretches or squeezes the text to the whole arc. Also works as a class. |
| `interactive` | | Makes the whole band clickable, even where it is transparent. See Pointer interaction. |
| `hit-width` | `0` | Extra touch area, in px, on each side of an `interactive` band. |
| Property or class | Default | What it does |
| `--o-fill` | `var(--o-gray-light)` | Band color. `transparent` for text only. |
| `--o-stroke` | `var(--o-fill)` | Outline color. |
| `--o-stroke-width` | `1` | Outline width, in px. |
| `color` or `--o-color` | `currentcolor` | Text color. Other font properties apply too. |
| `--o-gap`, `.gap-0` … `.gap-30` | `1` | Space at each end of the arc. Cascades from the ring and the system. |
| `--o-size-ratio` | `1` | Thickness, in lanes. `.grow-*` and `.shrink-*` work as on [satellites](https://zumerlab.com/orbit/reference/satellite.html). |
| `.inner-orbit`, `.outer-orbit`, `.quarter-*-orbit` | | Moves the band in or out from its lane. |
| `.angle-0` … `.angle-360` | | Starts the arc at that angle. |
## Split or stack
Without `value`, sibling arcs share the ring equally: a segmented ring, or a pie when they are thick. With `value`, each takes its share of the range and starts where the previous one ended: a donut chart. Values are not normalized, so make them add up to `max` for a full ring. With `max="24"`, `value="6"` is a quarter. An explicit `value="0"` draws nothing and takes no room.
```html
50
25
15
```
```css
.slices o-arc { --o-stroke: var(--bg); --o-stroke-width: 2; }
.capsule {
color: var(--bg);
font-size: 15px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
```
Five arcs with values 50, 25, 15, 5 and 5, and three satellites at the middle angle of the big ones.
## Color
`--o-fill` colors the band, and `--o-stroke` and `--o-stroke-width` its outline. They change with a short transition, so a hover state is one rule. Hover needs `interactive`: arcs ignore the pointer until you ask.
```html
```
```css
o-arc { cursor: pointer; }
o-arc.outlined {
--o-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: var(--accent);
--o-stroke-width: 2;
}
o-arc.plain {
--o-fill: var(--line);
}
/* One rule per state: the colors change with a short transition. */
o-arc.outlined:hover { --o-fill: var(--accent); }
o-arc.plain:hover { --o-fill: var(--amber); }
```
```js
document.getElementById('ring').innerHTML =
''.repeat(4);
```
Hover the segments.
## Endings
`shape` sets how each end looks: `circle` (or `circle-a`), `circle-b`, `bullet`, `arrow`, `slash`, `backslash`, `rounded` and `zigzag`. Endings are sized from the band, so they look the same on every ring and at every size.
```html
```
```css
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, 92px);
justify-content: center;
justify-items: center;
width: min(100%, 560px);
gap: 14px 24px;
}
.cell { display: grid; justify-items: center; gap: 4px; }
.cell code { color: var(--muted); }
.bigbang { width: 92px; }
.gravity-spot { --o-size-ratio: 3; --o-gap: 6; }
o-arc { --o-stroke: none; }
o-arc:nth-child(1) { --o-fill: color-mix(in srgb, var(--accent) 35%, var(--bg)); }
o-arc:nth-child(2) { --o-fill: color-mix(in srgb, var(--accent) 65%, var(--bg)); }
o-arc:nth-child(3) { --o-fill: var(--accent); }
```
```js
const shapes = ['none', 'circle', 'circle-b', 'bullet', 'arrow', 'slash', 'backslash', 'rounded', 'zigzag'];
// Three arcs per ring, all with the same ending.
document.getElementById('grid').innerHTML = shapes.map(shape => `
${``.repeat(3)}
${shape}
`).join('');
```
## Thickness, position and gaps
An arc is one lane thick. Set `--o-size-ratio` in lanes on the arc, its ring or the `.gravity-spot`, or use the `.grow-*` and `.shrink-*` classes on the arc. `.inner-orbit` and `.outer-orbit` move the band in or out so one of its edges sits on the lane, and the `.quarter-` versions go part of the way.
`--o-gap` is the space left at each end of an arc. It cascades, so one value on the ring spaces all its arcs; `.gap-0` to `.gap-30` set it on one arc. A ring of 3 to 6 thick segments usually wants `--o-gap` between 24 and 60 to read as separate pieces.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
/* The lane on top, so you can see where each band sits. */
.lane { border: 2px solid var(--amber); z-index: 1; }
.lane o-arc { --o-fill: color-mix(in srgb, var(--accent) 35%, transparent); --o-stroke: none; }
.gaps o-arc { --o-fill: var(--accent); --o-stroke: none; }
```
```js
const gap = document.getElementById('gap');
gap.addEventListener('input', () => {
document.getElementById('gaps').style.setProperty('--o-gap', gap.value);
document.getElementById('out').value = gap.value;
});
```
Outer ring, clockwise from the top: `outer-orbit`, `quarter-outer-orbit`, default, `quarter-inner-orbit`, `inner-orbit`. Inner ring: `--o-gap`.
Arcs follow the ring's `--o-range`, `--o-from` and `.ccw` (see [Orbits](https://zumerlab.com/orbit/reference/orbit.html#angles)). `.angle-N` on an arc starts it at that angle instead of its turn on the ring.
## Curved text
Text inside an `` is drawn along it. Style it like any text: `color`, `font-size`, `font-weight`, `text-transform`. The font size is the same in px on every ring. Set `--o-fill: transparent` for text with no band.
`text-anchor` places the text at the `start`, `middle` (default) or `end` of its arc.
```html
startmiddleend
No band, in bold
```
```css
o-arc {
--o-fill: color-mix(in srgb, var(--accent) 18%, transparent);
--o-stroke: none;
--o-gap: 4;
color: var(--text);
font-size: 14px;
}
o-arc.plain {
--o-fill: transparent;
color: var(--accent);
font-weight: 700;
}
```
Text runs clockwise, so on the lower half of a ring it reads upside down. `flip` runs it the other way:
```html
```
```css
o-arc {
--o-fill: color-mix(in srgb, var(--accent) 18%, transparent);
--o-stroke: none;
--o-gap: 3;
color: var(--text);
font-size: 13px;
}
o-arc[flip] {
--o-fill: color-mix(in srgb, var(--amber) 20%, transparent);
}
```
```js
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// April to September sit on the lower half: flip them so they read upright.
document.getElementById('months').innerHTML = months.map((month, i) =>
`= 3 && i <= 8 ? ' flip' : ''}>${month}`).join('');
```
April to September, in amber, have `flip`.
Text longer than its arc is cut off, and the console says so with an `[orbit]` message. Shorten it, widen the range, let the arc fit the text with `range="auto"`, or add `fit-range` to squeeze or stretch the text to the arc:
```html
This text runs along the whole arc
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
o-arc {
--o-fill: color-mix(in srgb, var(--accent) 18%, transparent);
--o-stroke: none;
color: var(--text);
font-size: 14px;
}
```
```js
const range = document.getElementById('range');
range.addEventListener('input', () => {
document.getElementById('ring').style.setProperty('--o-range', range.value + 'deg');
document.getElementById('out').value = range.value + '°';
});
```
Change the range: the text stretches or squeezes to fill it.
## Arcs that fit their text
With `range="auto"` an arc is as long as its text, with half a letter of room at each end. Consecutive auto arcs follow each other, which makes curved menus easy:
```html
Homeyou are here
```
```css
o-arc {
--o-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
--o-gap: 4;
color: var(--text);
font-size: 14px;
cursor: pointer;
}
o-arc:hover, o-arc:focus-visible { --o-fill: color-mix(in srgb, var(--accent) 32%, transparent); outline: none; }
.menu { border-color: var(--line); }
o-arc[aria-current] { --o-fill: var(--accent); color: var(--bg); font-weight: 600; }
.readout strong { font-size: 30px; }
```
```js
const items = document.querySelectorAll('.menu o-arc');
const pick = item => {
items.forEach(other => other.toggleAttribute('aria-current', other === item));
document.getElementById('page').textContent = item.textContent;
};
items.forEach(item => {
item.addEventListener('click', () => pick(item));
item.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); pick(item); } });
});
```
Three auto arcs, one after another. Click one, or tab to it and press Enter.
An auto arc starts at its angle. `text-anchor="middle"` centers it there instead, and `text-anchor="end"` ends it there. Both labels here are anchored at the top:
```html
Centered at 0°
Ends at 0°
```
```css
.lane { border: 1px dashed var(--line); }
o-arc {
--o-fill: color-mix(in srgb, var(--accent) 18%, transparent);
--o-stroke: none;
color: var(--text);
font-size: 14px;
}
.mark { height: 2px; background: var(--amber); }
```
The amber line points at 0°.
## Pointer interaction
Arcs are decorative by default, so they never steal clicks from controls nearby. `interactive` makes the whole band clickable, even where its fill is transparent, and `hit-width` adds touch area in px on each side, which helps with thin arcs on touch screens.
```html
```
```css
o-arc {
--o-fill: transparent;
--o-stroke: var(--accent);
--o-stroke-width: 2;
cursor: pointer;
}
o-arc:focus-visible { outline: none; --o-stroke-width: 3; }
o-arc[aria-checked="true"] { --o-stroke: var(--amber); }
```
```js
const arc = document.querySelector('o-arc');
const toggle = () => arc.setAttribute('aria-checked', String(arc.getAttribute('aria-checked') !== 'true'));
arc.addEventListener('click', toggle);
arc.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggle(); } });
```
Only the outline is drawn, yet a click anywhere on the band, or up to 12px around it, toggles the color.
When an arc acts as a control, you bring the keyboard behavior and the accessible name. Arcs are placed with the `rotate` property, so your own `transform` or `scale` (on hover, say) adds to it instead of replacing it.
## After drawing
Each `` fires a bubbling `o-rendered` event after it redraws. To wait for every arc on the page, for example before measuring, use `await Orbit.ready()`. See [Runtime and resizing](https://zumerlab.com/orbit/craft/runtime.html).
---
Source: https://zumerlab.com/orbit/reference/progress.html
# Progress
`` fills a lane up to a value, over a track that shows the whole range. It is the base of progress rings, gauges and meters.
```html
0%uploaded
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
.readout strong { font-size: 40px; }
```
```js
const bar = document.getElementById('bar'), input = document.getElementById('input');
const set = value => {
bar.setAttribute('value', value);
bar.setAttribute('aria-valuenow', value);
document.getElementById('value').textContent = value + '%';
};
input.addEventListener('input', () => set(input.value));
// Fill up on load.
const target = Number(input.value), start = performance.now();
const grow = now => {
const t = Math.min(1, (now - start) / 900);
set(Math.round(target * (1 - (1 - t) ** 3)));
if (t < 1) requestAnimationFrame(grow);
};
requestAnimationFrame(grow);
```
A progress goes straight into a ring, one per ring: put each bar of a multi-bar meter on its own ring. It is drawn by the runtime, so it needs the Orbit script. For screen readers, give it `role="progressbar"` and the `aria-` values, as above.
## Reference
| Attribute | Default | What it does |
| `value` | `0` | Progress, out of `max`. |
| `max` | `100` | The value that fills the ring's range. |
| `shape` | `none` | The ending. See Endings. |
| `variant` | | `stroke` draws thin lines instead of filled bands. See Stroke variant. |
| `interactive` | | Makes the whole track clickable, whatever its colors. See Pointer interaction. |
| `hit-width` | `0` | Extra touch area, in px, on each side of an `interactive` track. |
| Property or class | Default | What it does |
| `--o-progress` | | The value, set from CSS. Wins over `value`. |
| `--o-fill` | `var(--o-gray-light)` | Bar color. Not used by `variant="stroke"`. |
| `--o-stroke` | `var(--o-fill)` | Bar outline, or the line color with `variant="stroke"`. |
| `--o-stroke-width` | `1` (`2` with `stroke`) | Bar outline width, or the line width, in px. |
| `--o-back-fill` | `transparent` | Track color. Not used by `variant="stroke"`. |
| `--o-back-stroke` | `none` (gray with `stroke`) | Track outline, or the track line color. |
| `--o-back-stroke-width` | `1` | Track outline or line width, in px. |
| `--o-size-ratio` | `1` | Thickness, in lanes. `.grow-*` and `.shrink-*` work as on [satellites](https://zumerlab.com/orbit/reference/satellite.html). |
| `.inner-orbit`, `.outer-orbit`, `.quarter-*-orbit` | | Moves the band in or out from its lane. |
| `.angle-0` … `.angle-360` | | Starts the bar at that angle instead of the ring's `--o-from`. |
## Value and color
`value` runs from 0 to `max` (100 by default) and fills that share of the ring's range. `--o-fill` colors the bar and `--o-back-fill` the track behind it; `--o-stroke` and `--o-back-stroke` outline them.
`--o-progress` sets the value from CSS and wins over the attribute. Use it when a class or a parent owns the state:
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
o-progress { --o-stroke: none; }
o-progress.level {
--o-progress: 30;
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
}
.full o-progress.level {
--o-progress: 90;
}
o-progress.other {
--o-fill: var(--amber);
--o-back-stroke: var(--faint);
}
```
```js
const toggle = document.getElementById('toggle');
toggle.addEventListener('click', () => {
const full = document.querySelector('.bigbang').classList.toggle('full');
toggle.setAttribute('aria-pressed', full);
});
```
Outer ring: `--o-progress` from a class. Inner ring: `value="50" max="80"`, with an outlined track.
Orbit redraws when the element's or an ancestor's `class` or `style` changes. After a change it cannot see, such as editing a stylesheet rule, call `Orbit.refresh()`. The value does not animate: a `transition` on `--o-progress` jumps straight to the new value.
## Gauges
A progress fills the ring's range, so a partial range makes a gauge. `range-240 from-240` leaves the gap at the bottom:
```html
64km/h
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
o-progress {
--o-fill: var(--accent);
--o-back-fill: var(--line);
--o-stroke: none;
}
.readout strong { font-size: 36px; }
```
```js
const input = document.getElementById('input');
input.addEventListener('input', () => {
document.getElementById('bar').setAttribute('value', input.value);
document.querySelector('.bigbang').setAttribute('aria-valuenow', input.value);
document.getElementById('value').textContent = input.value;
});
```
## Endings
`shape` sets how the bar ends: `circle` (or `circle-a`), `circle-b`, `bullet`, `arrow`, `slash`, `backslash`, `rounded` and `zigzag`. Endings are sized from the band, so they look the same on every ring and at every size.
```html
```
```css
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, 92px);
justify-content: center;
justify-items: center;
width: min(100%, 560px);
gap: 14px 24px;
}
.cell { display: grid; justify-items: center; gap: 4px; }
.cell code { color: var(--muted); }
.bigbang { width: 92px; }
.gravity-spot { --o-size-ratio: 3; }
o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 18%, transparent);
--o-stroke: none;
}
```
```js
const shapes = ['none', 'circle', 'circle-b', 'bullet', 'arrow', 'slash', 'backslash', 'rounded', 'zigzag'];
document.getElementById('grid').innerHTML = shapes.map(shape => `
${shape}
`).join('');
```
## Stroke variant
By default the bar is a filled band. For a thin gauge line, add `variant="stroke"`: the bar and the track become lines along the middle of the lane, with round caps.
- `--o-stroke` colors the line and `--o-back-stroke` the track. `--o-fill` and `--o-back-fill` are not used.
- The line is 2px wide (`--o-stroke-width`) and the track 1px (`--o-back-stroke-width`), in screen pixels at any size.
- The track is gray by default; set `--o-back-stroke` to color it, or `none` to hide it.
- `shape` does not apply. A full value on a 360° range draws a closed circle.
```html
```
```css
o-progress { --o-stroke: var(--accent); }
o-progress.thick {
--o-stroke-width: 8;
--o-back-stroke: var(--line);
--o-back-stroke-width: 4;
}
o-progress.tinted {
--o-stroke: var(--amber);
--o-back-stroke: color-mix(in srgb, var(--amber) 30%, transparent);
}
o-progress.full { --o-stroke: var(--muted); }
o-progress.gauge {
--o-stroke-width: 4;
--o-back-stroke: var(--line);
}
```
From the outside in: an 8px line, a tinted track, a full circle, and a 270° gauge.
## Thickness and position
A progress is one lane thick. Set `--o-size-ratio` in lanes on the element, its ring or the `.gravity-spot`, or use the `.grow-*` and `.shrink-*` classes on the element. `.inner-orbit` and `.outer-orbit` move the band in or out so one of its edges sits on the lane, and the `.quarter-` versions go part of the way. A band two lanes thick on `orbit-11` ends exactly at the edge of the universe.
```html
```
```css
.stage { gap: 12px; align-content: center; }
.stage > .bigbang { width: min(100%, 100vh - 84px); }
o-progress {
--o-stroke: none;
--o-fill: var(--c);
--o-back-fill: color-mix(in srgb, var(--c) 16%, transparent);
}
.move { --c: var(--coral); }
.exercise { --c: var(--lime); }
.stand { --c: var(--accent); }
```
```js
const size = document.getElementById('size');
size.addEventListener('input', () => {
document.getElementById('system').style.setProperty('--o-size-ratio', size.value);
document.getElementById('out').value = size.value;
});
```
Three rings, their thickness set once on the `.gravity-spot`. At 2 lanes they touch each other and the edge.
## Pointer interaction
`` is decorative by default, so it never steals clicks from controls nearby. `interactive` makes the whole track clickable, including the part the bar does not cover, whatever its colors. `hit-width` adds touch area in px on each side, which helps with thin bars on touch screens.
```html
30click the ring
```
```css
o-progress {
--o-stroke: var(--accent);
--o-stroke-width: 4;
--o-back-stroke: var(--line);
--o-back-stroke-width: 4;
cursor: pointer;
}
o-progress:focus-visible { outline: none; --o-back-stroke: var(--faint); }
.readout strong { font-size: 30px; }
```
```js
const bar = document.getElementById('bar');
const set = value => {
bar.setAttribute('value', value);
bar.setAttribute('aria-valuenow', value);
document.getElementById('value').textContent = value;
};
// A click adds 10 and wraps after 100; the arrow keys step by 10 too.
bar.addEventListener('click', () => set((Number(bar.getAttribute('value')) + 10) % 110));
bar.addEventListener('keydown', event => {
const step = { ArrowUp: 10, ArrowRight: 10, ArrowDown: -10, ArrowLeft: -10 }[event.key];
if (step) { event.preventDefault(); set(Math.max(0, Math.min(100, Number(bar.getAttribute('value')) + step))); }
});
```
Each click on the thin track, or up to 12px around it, adds 10.
When a progress acts as a control, you bring the keyboard behavior and the accessible name. It is placed with the `rotate` property, so your own `transform` or `scale` adds to it instead of replacing it.
## After drawing
Each `` fires a bubbling `o-rendered` event after it redraws. To wait for everything on the page, for example before measuring, use `await Orbit.ready()`. See [Runtime and resizing](https://zumerlab.com/orbit/craft/runtime.html).
---
Source: https://zumerlab.com/orbit/craft/depth.html
# Depth layers
Add `depth` to a `.gravity-spot` and its rings stack on the z axis. The layer in front is the one people read and use; the others wait behind it.
Each direct child of the `.depth` element is a layer: a ring, or a nested `.gravity-spot` when a layer needs several rings of its own.
- `data-depth="1"` moves a layer one step back. Values go from `0` to `8`.
- `.layer-front` marks the active layer and places it at depth 0.
- `--o-z` sets the position directly, in unitless pixels. Negative values are farther away.
Layers keep their apparent size: the scale makes up for the perspective, `(perspective − z) / perspective`. So three rings on `orbit-6` would sit exactly on top of each other. `--o-depth-grow` makes each step back a bit bigger, so you can see them all. Depth changes animate.
```html
```
```css
body {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
height: 100vh;
padding: 16px 16px 14px;
}
.bigbang { max-height: 100%; }
.controls { justify-content: center; }
.gravity-spot {
--o-size-ratio: 1.2;
--o-depth-grow: .42;
--o-back-opacity: .5;
}
o-progress {
--o-stroke: none;
--o-fill: var(--faint);
--o-back-fill: var(--line);
}
.layer-front o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 16%, transparent);
}
```
```js
const layers = [...document.querySelectorAll('.depth > [data-depth]')];
let front = 0;
document.querySelector('#next').addEventListener('click', () => {
front = (front + 1) % layers.length;
layers.forEach((layer, i) => {
const depth = (i - front + layers.length) % layers.length;
layer.dataset.depth = depth;
layer.classList.toggle('layer-front', depth === 0);
});
});
```
## A whole system as a layer
A layer can hold several rings, a label and a face that move together. Make it a nested `.gravity-spot` with `data-depth`. It has the same reach as its parent, so it needs no `--o-force`, and it does not block the pointer outside its satellites.
Below, three gauges are stacked that way. Click a face to bring it forward, and move the pointer over the frame to tilt the stack.
```html
```
```css
body {
height: 100vh;
display: grid;
place-items: center;
}
/* The ancestor that owns the perspective. */
.tilt {
width: min(90vw, 260px);
perspective: 1000px;
}
.tilt > .bigbang {
rotate: x var(--rx, 0deg);
transform: rotateY(var(--ry, 0deg));
transition: rotate .2s, transform .2s;
}
/* Same value as the perspective above, without the unit. */
.stack {
--o-perspective: 1000;
--o-depth-grow: .24;
--o-back-opacity: .3;
}
.layer o-progress {
--o-stroke: none;
--o-fill: var(--c);
--o-back-fill: color-mix(in srgb, var(--c) 14%, var(--bg));
}
.face {
width: 100%;
aspect-ratio: 1;
border-radius: 50%;
display: grid;
place-content: center;
text-align: center;
background: color-mix(in srgb, var(--c) 10%, var(--panel));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 40%, transparent);
border: 0;
padding: 0;
color: var(--text);
cursor: pointer;
}
.face strong {
color: var(--c);
font-size: 30px;
font-variant-numeric: tabular-nums;
}
.face span {
font-size: 13px;
color: var(--muted);
}
```
```js
const layers = [...document.querySelectorAll('.stack > .layer')];
let front = 0;
const bring = (index) => {
front = (index + layers.length) % layers.length;
layers.forEach((layer, i) => {
const depth = (i - front + layers.length) % layers.length;
layer.dataset.depth = depth;
layer.classList.toggle('layer-front', depth === 0);
layer.style.zIndex = layers.length - depth;
});
};
layers.forEach((layer, i) => {
layer.querySelector('.face').addEventListener('click', () => bring(i === front ? i + 1 : i));
});
bring(0);
const tilt = document.querySelector('.tilt');
document.addEventListener('pointermove', (e) => {
tilt.style.setProperty('--ry', (e.clientX / innerWidth - .5) * 30 + 'deg');
tilt.style.setProperty('--rx', -(e.clientY / innerHeight - .5) * 30 + 'deg');
});
document.addEventListener('pointerleave', () => {
tilt.style.setProperty('--rx', '0deg');
tilt.style.setProperty('--ry', '0deg');
});
```
## Perspective from an ancestor
By default the `.depth` element owns the perspective. To tilt the whole object, as above, an ancestor has to own it instead: add `outer-perspective` next to `depth`, give the ancestor a `perspective`, and set `--o-perspective` to the same number so layers still keep their apparent size. Elements between that ancestor and `.depth` need `transform-style: preserve-3d`; a `.bigbang` that holds the `.depth` element gets it automatically.
```html
…
…
```
## Properties
Set these on the `.depth` element, or on a single layer.
| Property | Default | What it does |
| `--o-z` | `depth × step` | Position on the z axis in unitless pixels. Set it on a layer to override `data-depth`. |
| `--o-depth-step` | `-110` | Distance between depth steps. |
| `--o-perspective` | `900` | Perspective in unitless pixels. |
| `--o-depth-grow` | `0` | Extra size per step back. `.42` gives 1×, 1.42×, 1.84×, so no two layers share a radius. |
| `--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` | `.8s` | Duration of the depth transition. |
| `--o-depth-easing` | `cubic-bezier(.2,.8,.2,1)` | Easing of the depth transition. |
The transition is off when the user prefers reduced motion.
## Rules
- **Layers are direct children of `.depth`.** An element in between flattens the 3D context.
- **Rings outside `.depth` are not transformed.** `data-depth` alone does nothing.
- **Satellites, vectors, sides, `` and `` are never layers.** They keep their own transforms; put them in a ring and move the ring.
- **Back layers still receive clicks.** Rings ignore the pointer and satellites accept it, so a click on a satellite of a back layer reaches it. That is how the faces above come forward.
The [depth demo](https://zumerlab.com/orbit/demos/11-depth-layers.html) has the gauges above at full size, and [layered status rings](https://zumerlab.com/orbit/examples/layered-status-rings.html) a fuller example with alerts.
---
Source: https://zumerlab.com/orbit/craft/themes.html
# Themes
Orbit ships a plain default look, a cyan theme and a helper for building. All of them are written with `:where()`, so they have zero specificity apart from their own class: a single class of yours wins, without `!important`.
## Default theme
Always on. Satellites have no border or background, arcs and progress bands are light gray, and vectors and sides take the current text color. There are no hover colors.
```html
A
B
C
D
```
## Cyan theme
Add `theme-cyan` to the `.bigbang`, or to any ancestor. It outlines satellites, colors everything cyan and adds hover colors to arcs and progress bands.
```html
A
B
C
D
```
## dev-orbit
Add `dev-orbit` while you build. It draws the gravity spot, rings and satellites with dashed red lines, and outlines broken structure. It goes on top of a theme on the same element. See [Visual aids](https://zumerlab.com/orbit/craft/visual-aids.html).
```html
A
B
C
D
```
## Your own look
Style Orbit like anything else: satellites and capsules with ordinary CSS, arcs and progress with custom properties. The [color palette](https://zumerlab.com/orbit/craft/colors.html) is there if you want it. Here is the same layout once more, with a few rules of its own and a hover color on the satellites.
```html
A
B
C
D
```
```css
/* Your own look: ordinary CSS and Orbit's custom properties. */
.orbit-5 .satellite {
border-radius: 50%;
background: var(--panel);
box-shadow: inset 0 0 0 1px var(--line);
color: var(--text);
font-size: 13px;
}
.orbit-5 .satellite:hover { box-shadow: inset 0 0 0 1.5px var(--accent); color: var(--accent); }
o-arc { --o-fill: var(--line); --o-stroke: none; }
o-progress { --o-fill: var(--accent); --o-back-fill: color-mix(in srgb, var(--accent) 12%, transparent); --o-stroke: none; }
.vector { background: var(--faint); }
.side { background: var(--line); }
```
---
Source: https://zumerlab.com/orbit/craft/colors.html
# Colors
Orbit's stylesheet defines a palette of custom properties: ten hues, each in seven shades. The themes use it, and you can too, or ignore it and use your own colors.
The hues are `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `indigo`, `purple`, `pink` and `gray`. Each one comes as `--o-{hue}` plus six shades mixed from it in OKLab. For cyan:
| Property | Mix |
| `--o-cyan-white` | 95% white |
| `--o-cyan-lighter` | 75% white |
| `--o-cyan-light` | 30% white |
| `--o-cyan` | the hue itself |
| `--o-cyan-dark` | 20% black |
| `--o-cyan-darker` | 40% black |
| `--o-cyan-black` | 78% black |
Below, every shade on an arc: one ring per shade, from `-black` in the middle to `-white` outside, and one arc per hue. A few lines of JavaScript write the seventy arcs.
```html
`).join('');
```
## Your own color
`--o-color` is the eleventh hue, and it is yours. Set it on a system, a ring or a satellite, and `--o-color-white` to `--o-color-black` are mixed from it right there. Each ring below sets its own `--o-color` and the arcs use the same three shades: `-lighter`, `-light` and `-dark`.
```html
```
```css
.gravity-spot { --o-size-ratio: 2.4; }
o-arc { --o-gap: 3; --o-stroke: none; }
o-arc:nth-child(1) { --o-fill: var(--o-color-lighter); }
o-arc:nth-child(2) { --o-fill: var(--o-color-light); }
o-arc:nth-child(3) { --o-fill: var(--o-color-dark); }
```
Arcs and progress take the shades of their ring. On an `` itself, `--o-color` is the color of its text. To change one shade only, set it on the element that uses it.
---
Source: https://zumerlab.com/orbit/craft/visual-aids.html
# Visual aids
Add `dev-orbit` to a `.bigbang` while you build. It draws the gravity spot, the rings and the satellites with dashed red lines, outlines anything in the wrong place, and explains it in the console.
It changes how things look, never where they are, so you can switch it on and off freely. It combines with a theme on the same element. [Themes](https://zumerlab.com/orbit/craft/themes.html) shows a full layout with it on.
## Wrong children of a gravity spot
A `.gravity-spot` holds rings (`.orbit`, `.orbit-N`) and nested gravity spots, nothing else. If it holds anything else, the whole system is dimmed and a warning sign pulses at its center. Here a satellite sits directly in the gravity spot:
```html
A
B
C
```
```css
.orbit-8 {
--o-size-ratio: 2;
}
```
## Wrong children of a satellite or a ring
A `.satellite` holds a `.capsule` or a nested `.gravity-spot`. With anything else in it, it gets an orange dotted border and the console names the element. A ring nested in another ring is outlined the same way. This example logs on purpose:
```
[orbit]
sits directly in a .satellite. Put content in a .capsule so it stays upright and centered.
```
```html
A
C
```
```css
.orbit-8 {
--o-size-ratio: 2;
}
```
Content placed directly in a ring, instead of in a `.satellite`, is not outlined, but the console reports it. The full list of messages and their fixes is on the [install page](https://zumerlab.com/orbit/install.html#console).
The outlines use `:has()`. In a browser without it you still get the console messages. The pulsing sign stops when the user prefers reduced motion.
## Arcs on ellipses
`` and `` are drawn on circles only. On a ring stretched into an ellipse with `--o-ellipse-x` or `--o-ellipse-y`, they are hidden, and the console says so. Satellites and vectors follow the ellipse; sides do not. See [Orbits](https://zumerlab.com/orbit/reference/orbit.html#ellipses).
---
Source: https://zumerlab.com/orbit/craft/runtime.html
# Runtime and resizing
Most of the time there is nothing to call. Layouts follow their box, and the runtime notices changes on its own. The few functions it exports are for waiting, forcing an update, and old fixed-size layouts.
## Sizing needs no code
Every ring, satellite and band is a fraction of its `.bigbang`, so when the box changes size everything inside changes with it. There is no breakpoint to write. Drag the corner of the dashed box:
```html
```
```css
/* Drag the corner of the box. */
.wrapper {
width: 280px;
height: 200px;
margin: 16px;
border: 1px dashed var(--faint);
border-radius: 10px;
resize: both;
overflow: hidden;
}
.reach {
border: 1px dashed var(--accent);
}
.orbit-8 {
--o-size-ratio: 2;
}
.satellite {
border-radius: 50%;
background: var(--accent);
}
```
The `.bigbang` is square and as wide as its parent, and a parent with a height keeps it from growing taller. Here the box is 280 × 200, so the universe is 200px across. Give the `.bigbang` a `width`, `height` or `aspect-ratio` of its own and the system follows that instead.
## What the runtime watches
Orbit watches the page for added, removed and reordered elements, attribute and style changes, and containers that change size. It batches what it sees into the next animation frame. That is why a framework can re-render a layout, or you can change `value` on an ``, and the drawing follows with no extra call.
## Waiting for the drawing
`Orbit.ready()` returns a promise that resolves when every pending layout is drawn. Pass an element, a document or a shadow root to wait only for that part. Use it in tests, or before you measure something.
Each `` and `` also fires an `o-rendered` event after it redraws. It bubbles and crosses shadow roots, so one listener on a container hears all of its arcs.
```html
40%waiting
```
```css
body {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
height: 100vh;
padding: 16px 16px 14px;
}
.bigbang { max-height: 100%; }
.controls { justify-content: center; }
o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
.readout strong { font-size: 34px; }
```
```js
const progress = document.querySelector('o-progress');
let draws = 0;
// Fires after every redraw of the arc.
progress.addEventListener('o-rendered', () => {
draws++;
document.getElementById('value').textContent = progress.getAttribute('value') + '%';
document.getElementById('draws').textContent = `drawn ${draws}×`;
});
document.querySelector('input').addEventListener('input', (e) => {
progress.setAttribute('value', e.target.value);
progress.setAttribute('aria-valuenow', e.target.value);
});
Orbit.ready().then(() => console.log('ready'));
```
## Updating by hand
Two kinds of change are invisible to the runtime: rules you edit through CSSOM, and CSS that depends on something outside the layout, like a class on `` or a checkbox elsewhere on the page. After one of those, call `Orbit.refresh()` with the part that changed. Without an argument it refreshes the whole document.
```js
sheet.cssRules[0].style.setProperty('--o-range', '180deg');
Orbit.refresh(panel);
```
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.
## Fixed sizes and Orbit.resize()
When a system should keep a fixed size, give its `.gravity-spot` a length: `--o-force: 300px`. That system no longer follows its box. `Orbit.resize()` brings part of that back: it watches a container and scales the fixed-size systems inside it with its **width**, taking 500px as 1×.
```js
const stopResizing = Orbit.resize('.wrapper');
// When the container goes away:
stopResizing();
```
It takes a selector or an element and returns a function that stops watching. Calling it again on the same container replaces the previous watcher. It leaves systems without a fixed reach alone, and if it finds none the console says `[orbit] Orbit.resize() has nothing to do here`.
Here the same layout has `--o-force: 300px` and `Orbit.resize('.wrapper')`. At 280px wide the reach is 168px (300 × 280 / 500). Drag the corner: only the width counts.
```html
```
```css
/* Drag the corner of the box. */
.wrapper {
width: 280px;
height: 200px;
margin: 16px;
border: 1px dashed var(--faint);
border-radius: 10px;
resize: both;
overflow: hidden;
}
.reach {
border: 1px dashed var(--accent);
}
.orbit-8 {
--o-size-ratio: 2;
}
.satellite {
border-radius: 50%;
background: var(--accent);
}
```
```js
const stopResizing = Orbit.resize('.wrapper');
```
For new layouts, leave `--o-force` alone and size the `.bigbang` instead. `Orbit.resize()` is there mostly for layouts written for Orbit 1.x, where every system was 500px across. [Coming from 1.x](https://zumerlab.com/orbit/migrating.html) has the rest.
---
Source: https://zumerlab.com/orbit/frameworks/react.html
# React
Orbit is HTML and CSS with a runtime that watches the page, so in React you write the same markup with `className`. There is no wrapper component to install.
## Install
```sh
npm install @zumer/orbit
```
Import the stylesheet and the runtime once, in your entry file or in the component that uses Orbit:
```js
import '@zumer/orbit/style';
import '@zumer/orbit';
```
## A gauge
A progress ring with its value in the middle, bound to state. `value` on `` is an attribute, and React sets it like any other.
```jsx
import { useState } from 'react';
import '@zumer/orbit/style';
import '@zumer/orbit';
import './gauge.css';
export default function Gauge() {
const [value, setValue] = useState(72);
return (
{value}%
setValue(Number(e.target.value))} />
);
}
```
```css
/* gauge.css */
.gauge { width: 220px; }
.gauge .orbit-10 { --o-size-ratio: 2; }
.gauge .orbit-0 { --o-size-ratio: 12; }
.gauge o-progress {
--o-fill: var(--o-cyan);
--o-back-fill: var(--o-cyan-lighter);
--o-stroke: none;
}
.gauge .capsule { font-size: 28px; font-weight: 600; }
```
Sizes go in CSS rather than in `style`, so TypeScript does not complain about custom properties. Lists work as you expect: render satellites with `map()` and Orbit spreads however many there are.
```html
72%
```
```css
body {
height: 100vh;
display: grid;
place-items: center;
}
.gauge { width: 220px; display: grid; gap: 10px; }
.gauge .orbit-10 { --o-size-ratio: 2; }
.gauge .orbit-0 { --o-size-ratio: 12; }
.gauge o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
.gauge .capsule { font-size: 34px; font-weight: 650; letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
```
```js
const progress = document.querySelector('o-progress');
const label = document.querySelector('.capsule');
document.querySelector('input').addEventListener('input', (e) => {
progress.setAttribute('value', e.target.value);
label.textContent = e.target.value + '%';
});
```
The same markup in plain HTML. Move the slider: changing `value` is all it takes.
Orbit watches the page for added, removed and reordered elements, attribute and style changes, and containers that change size, so a re-render needs no extra call. To wait until arcs are drawn, or to update after a change Orbit cannot see, use the [runtime API](https://zumerlab.com/orbit/craft/runtime.html).
## TypeScript
The package ships JSX types for `` and ``. Load them once, for example in a `.d.ts` file of your app:
```ts
import type {} from '@zumer/orbit/react';
```
Then `` is checked like any other element, attributes included.
## Server rendering
Importing Orbit on the server is safe; it does nothing there. It has to run in the browser too, to register the elements and watch the page. In the Next.js App Router, import the stylesheet in `app/layout.tsx` and the runtime in a component marked `'use client'`.
---
Source: https://zumerlab.com/orbit/frameworks/vue.html
# Vue
In Vue you write Orbit's markup straight into templates. The only setup is telling the compiler that `` and `` are custom elements, not Vue components.
## Install
```sh
npm install @zumer/orbit
```
Import the stylesheet and the runtime in your entry file:
```js
// main.js
import '@zumer/orbit/style';
import '@zumer/orbit';
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
```
Then, in the Vite config:
```js
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('o-'),
},
},
}),
],
});
```
## A gauge
A progress ring with its value in the middle, bound to a ref:
```vue
{{ value }}%
```
```css
/* gauge.css */
.gauge { width: 220px; }
.gauge .orbit-10 { --o-size-ratio: 2; }
.gauge .orbit-0 { --o-size-ratio: 12; }
.gauge o-progress {
--o-fill: var(--o-cyan);
--o-back-fill: var(--o-cyan-lighter);
--o-stroke: none;
}
.gauge .capsule { font-size: 28px; font-weight: 600; }
```
Satellites rendered with `v-for` are spread around their ring however many there are.
```html
72%
```
```css
body {
height: 100vh;
display: grid;
place-items: center;
}
.gauge { width: 220px; display: grid; gap: 10px; }
.gauge .orbit-10 { --o-size-ratio: 2; }
.gauge .orbit-0 { --o-size-ratio: 12; }
.gauge o-progress {
--o-fill: var(--accent);
--o-back-fill: color-mix(in srgb, var(--accent) 14%, transparent);
--o-stroke: none;
}
.gauge .capsule { font-size: 34px; font-weight: 650; letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
```
```js
const progress = document.querySelector('o-progress');
const label = document.querySelector('.capsule');
document.querySelector('input').addEventListener('input', (e) => {
progress.setAttribute('value', e.target.value);
label.textContent = e.target.value + '%';
});
```
The same markup in plain HTML. Move the slider: changing `value` is all it takes.
Orbit watches the page for added, removed and reordered elements, attribute and style changes, and containers that change size, so a re-render needs no extra call. To wait until arcs are drawn, or to update after a change Orbit cannot see, use the [runtime API](https://zumerlab.com/orbit/craft/runtime.html).
## Nuxt
Importing Orbit on the server is safe. Put the stylesheet in `css`, the custom element rule in `vue.compilerOptions`, and the runtime in a client plugin:
```js
// nuxt.config.ts
export default defineNuxtConfig({
css: ['@zumer/orbit/style'],
vue: {
compilerOptions: { isCustomElement: (tag) => tag.startsWith('o-') },
},
});
// app/plugins/orbit.client.ts
import '@zumer/orbit';
export default defineNuxtPlugin(() => {});
```
In Nuxt 3 the plugin is `plugins/orbit.client.ts`, at the root. Nuxt 4 ignores that folder, so there the elements would never be defined.
## Without a build step
With Vue from a CDN, load Orbit's two files with `` and `
{value}%
```
```css
/* gauge.css */
.gauge { width: 220px; }
.gauge .orbit-10 { --o-size-ratio: 2; }
.gauge .orbit-0 { --o-size-ratio: 12; }
.gauge o-progress {
--o-fill: var(--o-cyan);
--o-back-fill: var(--o-cyan-lighter);
--o-stroke: none;
}
.gauge .capsule { font-size: 28px; font-weight: 600; }
```
In Svelte 4, write `let value = 72` instead of `$state(72)`. The CSS can also live in the component's `
```
To load Orbit from a CDN instead, put the `` and `
```
The entry registers every component. To load only some, import their files: `import '@zumer/orbit-kit/o-gauge.js'`. Orbit 2 is a peer dependency; the kit does not work with Orbit 1.x.
## Components
- [Gauge](https://zumerlab.com/orbit/kit/gauge.html): One value on an arc.
- [Donut](https://zumerlab.com/orbit/kit/donut.html): A donut chart from an array.
- [Rings](https://zumerlab.com/orbit/kit/rings.html): Concentric activity rings.
- [Knob](https://zumerlab.com/orbit/kit/knob.html): A rotary control.
- [Menu](https://zumerlab.com/orbit/kit/menu.html): A radial menu, flat or nested.
- [Instrument](https://zumerlab.com/orbit/kit/instrument.html): Needle gauges with presets.
- [Clock](https://zumerlab.com/orbit/kit/clock.html): Clocks and watch faces.
- [Compass](https://zumerlab.com/orbit/kit/compass.html): A heading indicator.
- [Radar](https://zumerlab.com/orbit/kit/radar.html): A scope with a sweep and blips.
- [Timer](https://zumerlab.com/orbit/kit/timer.html): A countdown ring with controls.
- [Status](https://zumerlab.com/orbit/kit/status.html): Health of many things, in layers.
- [Range](https://zumerlab.com/orbit/kit/range.html): Two handles on one ring: a span of values or hours.
- [Time picker](https://zumerlab.com/orbit/kit/time-picker.html): Pick a time by turning a clock.
- [Spinner](https://zumerlab.com/orbit/kit/spinner.html): Loading rings, dots and orbits.
- [Radar chart](https://zumerlab.com/orbit/kit/radar-chart.html): Values across axes, on a web.
- [Sunburst](https://zumerlab.com/orbit/kit/sunburst.html): A tree as nested rings, with zoom.
- [Wheel](https://zumerlab.com/orbit/kit/wheel.html): A prize wheel that spins to a result.
- [Color wheel](https://zumerlab.com/orbit/kit/color-wheel.html): A hue ring color picker.
## What they share
Once you know one component, you know most of the others.
| Attribute | What it does |
| `size` | Width of the widget, as any CSS length. Most are square; `100%` follows the container as it resizes. |
| `thickness` | Band thickness in lanes, Orbit's unit: a lane is a twelfth of the radius. |
| `fill`, `track` | Value and track colors of the band components: gauge, knob and timer. |
| `accent` | The highlight of the dial components: instrument, clock, compass and menu. |
| `label`, `unit` | The text in the middle. |
| `range`, `from` | Arc span and start angle in degrees. Partial ranges center their gap at the bottom. |
| `glow` | A glow in the indicator color; `glow="12"` sets the blur in px. |
| `data`, `items`, `targets`, `zones` | Lists, as JSON in the attribute. |
| `min`, `max`, `step` | The range of a value, read like ``. |
| `format`, `locale` | How numbers are written: percent, currency, units, compact. |
| `theme` | `dark`, `light` or `glass`, or an `ok-theme-*` class on a container. |
| `name` | The knob and the menu are form controls. |
The last four work the same everywhere; [Shared options](https://zumerlab.com/orbit/kit/options.html) has the details and live examples.
Change an attribute and the component updates. Values only move what moves; a new structure, like a different number of segments, rebuilds the inside.
Colors live in custom properties that inherit, so one rule themes a whole panel:
```
.dashboard {
--ok-fill: var(--accent); /* value arcs */
--ok-track: color-mix(in oklab, var(--accent) 22%, #0c0f13); /* tracks */
--ok-accent: #f97316; /* needles, hands, highlights */
--ok-text: #e2e8f0; /* readouts */
--ok-face: #0b1220; /* dials */
}
```
Each page lists the properties of its component. Attributes set the same properties on the element itself, so an attribute wins over a rule on a parent.
Text inside a component scales with it: the base font size is a share of the widget (`--ok-font-size`), and readouts are sized in `em`. A 120px gauge and a 400px gauge look the same, only bigger.
Every component has a role and a name that follow its value: gauges and instruments are meters, the knob is a slider, the menu is a menu, the timer is a timer. Write your own `aria-label` and the component keeps it. Decorative motion stops when the system asks for reduced motion.
## Coming from 0.x
Tag names and attributes from 0.x still work. What changed is underneath:
- The kit runs on Orbit 2 and no longer works with 1.x.
- Components are sized by their universe. The outer band touches the edge of the widget, so a gauge looks a little bigger in the same `size`.
- `thickness` sets band thickness. `orbit` and `stroke-width` still work as before.
- `gap` is now a share of the widget width, the same in the donut, the menu and the status ring.
- Colors are custom properties you can set from CSS, and text scales with the widget.
- New: [``](https://zumerlab.com/orbit/kit/timer.html) and [``](https://zumerlab.com/orbit/kit/status.html), `min` on the gauge, `accent` on the compass and the radar.
- `` still ships; [``](https://zumerlab.com/orbit/kit/instrument.html) does all it does and more.
---
Source: https://zumerlab.com/orbit/kit/options.html
# Shared options
Themes, number format, `min`, `max` and `step`, and forms work the same in every kit component that has them.
## Themes
Put `theme="dark"`, `"light"` or `"glass"` on a component, or the class `ok-theme-dark`, `ok-theme-light` or `ok-theme-glass` on a container. Everything inside follows, so one class themes a whole dashboard.
```html
Dark
Light
Glass
```
```css
body { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; align-items: center; padding: 16px; }
.panel { display: grid; justify-items: center; gap: 10px; padding: 18px 22px 12px; border-radius: 16px; border: 1px solid var(--line); }
.panel p { margin: 0; font-size: 13px; color: var(--ok-text); opacity: .7; }
.ok-theme-dark { background: var(--panel); }
.ok-theme-light { background: #f6f7f9; }
.ok-theme-glass { background: linear-gradient(140deg, #6d28d9, #0ea5e9 60%, #14b8a6); }
```
```js
// Each knob drives the gauge in its panel.
for (const panel of document.querySelectorAll('.panel')) {
const gauge = panel.querySelector('o-gauge');
panel.querySelector('o-knob').addEventListener('input', (e) => gauge.setAttribute('value', e.detail.value));
}
```
Turn a knob: the gauge next to it follows.
- A theme is a set of the `--ok-*` custom properties, inside `:where()`. It has zero specificity, so any rule of yours wins, and color attributes like `fill` win too.
- Custom properties inherit, so the closest theme wins: a `theme="light"` gauge inside an `ok-theme-dark` panel is light.
- Without a theme, components keep their own defaults.
- Glass makes faces and panels translucent and blurs what is behind them (`backdrop-filter`). It wants a colorful or photographic background.
The properties a theme sets, and what reads them:
| Property | What it colors |
| `--ok-fill`, `--ok-track` | Value arcs and their tracks. |
| `--ok-accent` | Needles, second hands, compass index, menu highlight. |
| `--ok-text` | Readouts, numerals and labels. |
| `--ok-face`, `--ok-bezel`, `--ok-case` | Dials, their rims and the case around them. |
| `--ok-tick`, `--ok-hand` | Minor ticks; hands, major ticks and strong numbers. |
| `--ok-surface` | Panels such as the menu hub and the needle cap. |
| `--ok-focus` | Focus rings. |
| `--ok-backdrop` | The `backdrop-filter` of faces and panels. Set by glass. |
To tweak a theme, set a property next to it:
```
.dashboard { --ok-fill: #ffb547; } /* beats the theme */
…
```
The menu already had a `theme` attribute for its sector colors. That is now `palette="mono|color"`; `theme="color"` still works and combines with the others, as in `theme="color light"`.
## Number format
`format` picks how numbers are written, through `Intl.NumberFormat`. It reaches the readout, the tick labels and `aria-valuetext`.
```html
```
```css
.stage { gap: 14px; align-content: center; }
.controls { justify-content: center; }
#formats { display: flex; flex-wrap: wrap; gap: 6px; }
button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
select { font: inherit; background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px; }
```
```js
const dial = document.getElementById('dial');
// Each button is a set of attributes; "plain" removes them all.
const formats = {
plain: {},
decimal: { format: 'decimal', decimals: '1' },
percent: { format: 'percent' },
currency: { format: 'currency', currency: 'EUR' },
unit: { format: 'unit', unit: 'megabyte' },
compact: { format: 'compact' }
};
const buttons = document.getElementById('formats');
for (const name of Object.keys(formats)) {
const button = document.createElement('button');
button.textContent = name;
button.setAttribute('aria-pressed', name === 'plain');
button.onclick = () => {
for (const attr of ['format', 'decimals', 'currency', 'unit']) dial.removeAttribute(attr);
for (const [attr, value] of Object.entries(formats[name])) dial.setAttribute(attr, value);
for (const other of buttons.children) other.setAttribute('aria-pressed', other === button);
};
buttons.append(button);
}
document.getElementById('locale').onchange = (e) => dial.setAttribute('locale', e.target.value);
```
Switch the format and the locale: the readout and the scale change together.
| Attribute | What it does |
| `format` | `decimal`, `percent`, `currency`, `unit` or `compact` (1.2K, 3.4M). |
| `decimals` | Digits after the decimal point, fixed. Without it, each format uses its usual number. |
| `currency` | ISO code for `format="currency"`, `USD` by default. Setting it alone implies the currency format. |
| `unit` | Text after the number, as before (`unit="%"`). With `format="unit"` it names an Intl unit instead: `kilometer-per-hour`, `celsius`, `megabyte`. |
| `locale` | A language tag like `de-DE`. Without it, the closest `lang` attribute, then the browser's language. |
`percent` shows where the value sits between `min` and `max`: 50 on a 0 to 200 gauge reads 25%. In a donut it is the share of the total. Without any of these attributes, numbers look the way they did in 1.0. A bad currency or unit never breaks the component: it falls back to a plain number.
### A formatter for full control
Set `formatter` to a function and it writes every number. The second argument says where the number goes: `kind` is `value`, `tick` or `aria`, and you get `min`, `max` and `locale`. Its text is used as is, without the unit.
```html
```
```css
body { display: grid; place-items: center; height: 100vh; }
```
```js
// Minutes as "1h 35m" on screen, and in words for screen readers.
document.getElementById('time').formatter = (minutes, { kind }) => {
const h = Math.floor(minutes / 60), m = Math.round(minutes % 60);
return kind === 'aria' ? `${h} hours ${m} minutes` : `${h}h ${String(m).padStart(2, '0')}m`;
};
```
## min, max and step
Every component with a value reads `min`, `max` and `step` the way `` does.
```html
Screen readers hear
```
```css
.stage { gap: 6px; align-content: center; }
.controls { margin: 0; }
```
```js
const trim = document.getElementById('trim'), said = document.getElementById('said');
const show = () => { said.textContent = trim.getAttribute('aria-valuetext'); };
trim.addEventListener('input', show);
customElements.whenDefined('o-knob').then(show);
```
Drag, scroll or use the arrow keys: every value lands on a half step.
- Values are clamped to the range, readouts included. A `max` below `min` becomes `min`.
- `step` snaps values to a grid that starts at `min`. Both ends stay reachable, even when the step does not divide the range. `step="any"` turns snapping off.
- Display components (gauge, meter, instrument) do not snap unless you give them a `step`. The knob steps by 1.
- Keys: arrows move one step, Page Up and Page Down ten, Home and End go to the ends. Without a step they move 1% of the range.
- `aria-valuemin`, `aria-valuemax`, `aria-valuenow` and `aria-valuetext` follow, the last one through the number format.
Rings read `min` and `max` as defaults for each ring, and a ring in `data` can set its own. The radar reads `dist` between them, so blips can be in kilometers. On the rings, `step` was already the space between rings; it keeps that meaning, and `spacing` is its new name.
## Forms
`` and `` are form controls. Give them a `name` and they submit with their form like an ``.
```html
```
```css
.stage { gap: 14px; align-content: center; }
.row { display: flex; gap: 28px; align-items: center; justify-content: center; }
.controls { justify-content: center; }
```
```js
const form = document.getElementById('order'), sent = document.getElementById('sent');
form.addEventListener('submit', (e) => {
e.preventDefault();
sent.textContent = new URLSearchParams(new FormData(form)).toString();
});
form.addEventListener('reset', () => { sent.textContent = ''; });
```
Order without a size and the browser points at the menu. Reset puts the knob back on 3.
| Name | What it does |
| `name` | The field name in the form data. Without it, nothing is sent. |
| `value` | The attribute is the default value: the control starts there and a form reset goes back to it. The property is the current value; setting it fires no event. |
| `disabled` | Out of the tab order, ignores input and is not sent. A disabled `