Detent web components
By Shane Logsdon

Media & effects

Circular Orbit

Implemented

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

  • hover recorded state; no setter
  • loading recorded state; no setter
  • paused recorded state; no setter
  • reduced motion recorded 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-name
  • pause-on-hover
  • radius
  • speed
  • title
  • title-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.tsx sha256 dcb1498e359f…

Native files

The native module and its artifacts:

  • src/components/circular-orbit/circular-orbit.ts
  • src/components/circular-orbit/styles.css
  • src/components/circular-orbit/define.ts
  • docs/components/circular-orbit.md
  • site/examples/circular-orbit.ts
  • tests/browser/circular-orbit.spec.ts
  • evidence/components/circular-orbit.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
container-class-name———Recorded in the catalog as an attribute.
pause-on-hoverpauseOnHoverbooleantruePresence semantics: present means true, absent means false.
radiusradiusnumber270Kebab-case attribute; camelCase property.
speedspeednumber0.00022Kebab-case attribute; camelCase property.
titletitlestring'Push'Kebab-case attribute; camelCase property.
title-class-name———Recorded in the catalog as an attribute.

Properties

Properties
PropertyTypeDefaultNotes
itemsOrbitItem[]14 Unsplash tilesProperty-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.
Previous: Chat Next: Code Snippet Deck

Docs

Components

Project