Umlaufbahn
Icons, Logos oder Avatare, die auf Ringen um eine Mitte kreisen und dabei aufrecht bleiben.
Eine von 6 Components in Beams, Partikel und Bewegung
Details
Was Umlaufbahn mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Beams, Partikel und Bewegung
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/orbit.tsx
- Bringt mit
- utilskit
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle OrbitProps aus components/ui/orbit.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
ringsPflichtOrbitRing[]centeroptionalReact.ReactNodeWhat sits in the middle, e.g. your app mark
sizeoptionalnumberLargest size in px, the orbit shrinks with its container below that. Default 520
showRingsoptionalbooleanDraw the rings as hairlines, default true
dashedoptionalbooleanDashed rings instead of solid
scrollTurnoptionalnumberDegrees the rings turn on top of their run over a full scroll pass, items stay upright. Default 0 (off), 60 reads well.
labeloptionalstringRead out to screen readers, e.g. "Connects with 12 tools". Without it the orbit is decorative.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 125 Zeilen
'use client';
import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion, round } from '@/lib/kit';
export interface OrbitRing {
/** Icons, logos or avatars, spread evenly around the ring */
items: React.ReactNode[];
/** Ring radius in % of the orbit size, 0 to 50 */
radius: number;
/** Seconds for one full turn, default 40 */
duration?: number;
/** Turn counterclockwise */
reverse?: boolean;
/** Start angle of the first item in degrees, default -90 (top) */
offset?: number;
}
export interface OrbitProps {
rings: OrbitRing[];
/** What sits in the middle, e.g. your app mark */
center?: React.ReactNode;
/** Largest size in px, the orbit shrinks with its container below that. Default 520 */
size?: number;
/** Draw the rings as hairlines, default true */
showRings?: boolean;
/** Dashed rings instead of solid */
dashed?: boolean;
/** Degrees the rings turn on top of their run over a full scroll pass, items stay upright. Default 0 (off), 60 reads well. */
scrollTurn?: number;
/** Read out to screen readers, e.g. "Connects with 12 tools". Without it the orbit is decorative. */
label?: string;
className?: string;
}
const CSS = `
@keyframes kitfx-orbit-spin { to { transform: rotate(360deg); } }
.kitfx-orbit * { animation-play-state: var(--kitfx-play, running); }
.kitfx-orbit-turn { rotate: calc((var(--kitfx-p, 0.5) - 0.5) * var(--kitfx-turn)); transition: rotate .3s linear; }
.kitfx-orbit-upright { rotate: calc((var(--kitfx-p, 0.5) - 0.5) * var(--kitfx-turn) * -1); transition: rotate .3s linear; }
@media (prefers-reduced-motion: reduce) { .kitfx-orbit * { animation: none !important; } .kitfx-orbit-turn, .kitfx-orbit-upright { rotate: none; } }
`;
/**
* Items circling a center on one or more rings. Items stay upright while
* their ring turns. Each item wrapper carries data-orbit-item, e.g. to hide
* the items on phones with max-sm:[&_[data-orbit-item]]:hidden. Square, responsive up to `size`. Pauses out of view and
* stands still with reduced motion.
*/
export function Orbit({ rings, center, size = 520, showRings = true, dashed, scrollTurn = 0, label, className }: OrbitProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el || typeof IntersectionObserver === 'undefined') return;
const io = new IntersectionObserver(([e]) => el.style.setProperty('--kitfx-play', e.isIntersecting ? 'running' : 'paused'));
io.observe(el);
return () => io.disconnect();
}, []);
// Scroll pass: 0 when the orbit enters at the bottom, 1 when it leaves at the top
useEffect(() => {
const el = ref.current;
if (!el || !scrollTurn || prefersReducedMotion()) return;
let frame = 0;
const update = () => {
frame = 0;
const r = el.getBoundingClientRect();
el.style.setProperty('--kitfx-p', Math.min(1, Math.max(0, (window.innerHeight - r.top) / (r.height + window.innerHeight))).toFixed(4));
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', onScroll, { passive: true });
return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', onScroll);
};
}, [scrollTurn]);
return (
<div
ref={ref}
{...(label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true })}
className={cn('kitfx-orbit relative mx-auto aspect-square w-full', className)}
style={{ maxWidth: size, ...(scrollTurn ? { ['--kitfx-turn' as string]: `${scrollTurn}deg` } : {}) }}
>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
{showRings &&
rings.map((r, i) => (
<div
key={`r${i}`}
className={cn('absolute rounded-full border', dashed ? 'border-dashed border-[color-mix(in_oklch,var(--foreground)_16%,transparent)]' : 'border-[color-mix(in_oklch,var(--foreground)_11%,transparent)]')}
style={{ left: `${50 - r.radius}%`, top: `${50 - r.radius}%`, width: `${r.radius * 2}%`, height: `${r.radius * 2}%` }}
/>
))}
{rings.map((r, i) => {
const dur = r.duration ?? 40;
const dir = r.reverse ? 'reverse' : 'normal';
const counter = r.reverse ? 'normal' : 'reverse';
return (
<div key={`o${i}`} className={cn('absolute inset-0', scrollTurn && 'kitfx-orbit-turn')} style={{ animation: `kitfx-orbit-spin ${dur}s linear infinite ${dir}`, ...(r.reverse && scrollTurn ? { ['--kitfx-turn' as string]: `${-scrollTurn}deg` } : {}) }}>
{r.items.map((item, k) => {
const a = round((r.offset ?? -90) + 90 + (360 / r.items.length) * k, 2);
return (
<div key={k} data-orbit-item="" className="absolute inset-0" style={{ transform: `rotate(${a}deg)` }}>
<div className="absolute left-1/2" style={{ top: `${50 - r.radius}%`, transform: `translate(-50%, -50%) rotate(${-a}deg)` }}>
<div className={scrollTurn ? 'kitfx-orbit-upright' : undefined} style={{ animation: `kitfx-orbit-spin ${dur}s linear infinite ${counter}` }}>{item}</div>
</div>
</div>
);
})}
</div>
);
})}
{center && <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">{center}</div>}
</div>
);
}
/** A round chip for orbit items: an icon or initials on a card surface */
export function OrbitItem({ className, children }: { className?: string; children: React.ReactNode }) {
return (
<span className={cn('grid size-11 place-items-center rounded-full border border-border bg-card text-foreground shadow-[var(--highlight),var(--shadow-md)] sm:size-12', className)}>{children}</span>
);
}Häufige Fragen
Wie installiere ich Umlaufbahn?
Mit aiw add @ai-website/orbit. Der Befehl legt die Datei components/ui/orbit.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Umlaufbahn?
3 Varianten, Integrationen, Halo hinter dem Text und Menschen.
Kann ein KI-Assistent Umlaufbahn einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Umlaufbahn mit aiw_components und installieren sie mit aiw_add @ai-website/orbit.