Media & effects
Circular Orbit
Continuous circular orbit gallery with depth-aware tiles
detent-circular-orbit
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 4
- Wave
- phase-4-cinematic
- Group
- Media & effects
5.7 KB gzip for the class module
Variants
The pinned demo dataset (14 Unsplash tiles) orbits the headline. Every tile derives its x/y from one rotation value,
so the ring is a true circle at
radius 270 and the depth field (scale 1 → 0.42, opacity 1 → 0.42, blur 0 → 1.2px) follows
cos(angle). Hovering pauses the orbit with a 120 ms ease; leaving resumes it.
A five-tile items dataset at radius 150 and a slower speed 0.00008.
items is property-only, so the demo sets it through the DOM like a consumer would.
With prefers-reduced-motion: reduce the loop never starts: the gallery rests at its starting pose,
schedules no animation frame and stays fully legible. The headline and the region label come from
title; the tiles and the headline element are aria-hidden, so assistive technology
announces one region.
Recorded states
hoverrecorded state; no setterloadingrecorded state; no setterpausedrecorded state; no setterreduced motionrecorded state; no setter
Usage
Import
import '/components/circular-orbit/define.js';
// or register it yourself
import { DetentCircularOrbit } from '/components/circular-orbit/circular-orbit.js';
Attributes
container-class-namepause-on-hoverradiusspeedtitletitle-class-name
Properties
items
Slots
No slots are recorded for this component.
Events
No events are recorded for this component.
Example
Runnable example: site/examples/circular-orbit.ts. The snippet below is generated from the catalogued attributes.
<detent-circular-orbit
container-class-name
pause-on-hover
radius="270"
speed="0.00022"
title="Push"
title-class-name
></detent-circular-orbit>
Property-only values
const element = document.querySelector('detent-circular-orbit');
if (element !== null) {
// items: OrbitItem[]
element.items = [];
}
Source
The pinned upstream source for this row:
src/components/ui/CircularOrbit.tsxsha256 dcb1498e359f…
Native files
The native module and its artifacts:
src/components/circular-orbit/circular-orbit.tssrc/components/circular-orbit/styles.csssrc/components/circular-orbit/define.tsdocs/components/circular-orbit.mdsite/examples/circular-orbit.tstests/browser/circular-orbit.spec.tsevidence/components/circular-orbit.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
container-class-name | — | — | — | Recorded in the catalog as an attribute. |
pause-on-hover | pauseOnHover | boolean | true | Presence semantics: present means true, absent means false. |
radius | radius | number | 270 | Kebab-case attribute; camelCase property. |
speed | speed | number | 0.00022 | Kebab-case attribute; camelCase property. |
title | title | string | 'Push' | Kebab-case attribute; camelCase property. |
title-class-name | — | — | — | Recorded in the catalog as an attribute. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
items | OrbitItem[] | 14 Unsplash tiles | Property-only: assign from script; not accepted as an attribute. |
Events
No custom events are recorded for this component.
Slots
No slots are recorded for this component.
CSS customisation
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Hover-pause uses a time-based ease: the upstream fixed 0.08/frame lerp is frame-rate dependent and never reaches zero, so a paused orbit could never stop scheduling frames.
- container-class-name and title-class-name are applied to shadow parts and styled through ::part().
- The loop pauses while the document is hidden or the element is offscreen (PC-10.6).
- items[].src is scheme-checked (R-3.2): the safeImageUrl allow-list refuses javascript:, data:, vbscript: and file:.
- title is the recorded heading attribute and therefore shadows the global title tooltip; the heading renders in the header, not as a tooltip.
What it does
A circular gallery of decorative tiles orbiting a centred headline. One rotation value drives every tile, so the depth field stays internally consistent.
Upstream behaviour
- Single shared rotation MotionValue drives every tile — no React state per frame
- True circular orbit: x and y use the same radius so tiles trace a perfect circle
- Depth field derived from cos(angle): scale, opacity, blur, and z-index stay internally consistent
- Smooth ease-in / ease-out on hover-pause instead of a hard stop
- Configurable speed, radius, items, and pause-on-hover behaviour
- Respects prefers-reduced-motion: animation loop short-circuits to a static rest pose
- Light and dark surface variants
Differences from upstream
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Hover-pause uses a time-based ease: the upstream fixed 0.08/frame lerp is frame-rate dependent and never reaches zero, so a paused orbit could never stop scheduling frames.
- container-class-name and title-class-name are applied to shadow parts and styled through ::part().
- The loop pauses while the document is hidden or the element is offscreen (PC-10.6).
- items[].src is scheme-checked (R-3.2): the safeImageUrl allow-list refuses javascript:, data:, vbscript: and file:.
- title is the recorded heading attribute and therefore shadows the global title tooltip; the heading renders in the header, not as a tooltip.