← каталог
Orbiting Circles
Узлы едут по круговым орбитам: угол каждого задаёт отрицательная задержка одной общей анимации, контр-вращение держит их вертикально. Чистый CSS.
cssautobasic0reduced-motion ✓
🎛 Настройки — крути и копируй
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| radius? | number | 110 | Радиус орбиты, px. |
| duration? | number | 20 | Секунд на полный оборот. |
| reverse? | boolean | false | Крутить в обратную сторону. |
| iconSize? | number | 40 | Размер узла на орбите, px. |
| ring? | boolean | true | Рисовать ли само кольцо орбиты. |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/orbiting-circles.jsonЧто нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
import { Children } from "react";
import type { CSSProperties, ReactNode } from "react";
export interface OrbitingCirclesProps {
/** Радиус орбиты, px. */
radius?: number;
/** Секунд на полный оборот. */
duration?: number;
/** Крутить в обратную сторону. */
reverse?: boolean;
/** Размер узла на орбите, px. */
iconSize?: number;
/** Рисовать ли само кольцо орбиты. */
ring?: boolean;
/** Узлы, которые едут по орбите; раскладываются по кругу равномерно. */
children?: ReactNode;
}
/**
* Узлы едут по круговой орбите. Хитрость раскладки: у всех узлов ОДНА анимация
* вращения, а угол каждого задаёт отрицательная animation-delay (−i/n цикла) —
* ни одного вычисления угла ни в JS, ни в CSS. Контр-вращение с той же задержкой
* держит узел вертикально. Reduced-motion / data-shot — узлы стоят на кольце.
*/
export function OrbitingCircles({
radius = 110,
duration = 20,
reverse = false,
iconSize = 40,
ring = true,
children,
}: OrbitingCirclesProps = {}) {
const items = Children.toArray(children);
const n = Math.max(1, items.length);
return (
<div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
<style>{`
.oc-spin {
position: absolute; left: 50%; top: 50%;
animation: oc-rotate var(--oc-dur) linear infinite;
animation-delay: var(--oc-delay);
animation-direction: var(--oc-dir);
}
/* контр-вращение — та же длительность и задержка, направление противоположное:
сумма углов всегда ноль, узел не кувыркается */
.oc-keep {
display: grid; place-items: center;
width: var(--oc-size); height: var(--oc-size);
margin: calc(var(--oc-size) / -2);
border-radius: 999px;
background: oklch(0.21 0.02 270);
border: 1px solid oklch(0.34 0.02 270);
animation: oc-rotate var(--oc-dur) linear infinite reverse;
animation-delay: var(--oc-delay);
animation-direction: var(--oc-keep-dir);
}
/* from задан ЯВНО: пока анимация жива, она полностью владеет углом, а базовый
transform (инлайн, угол i/n) показывается только при animation: none —
без него в reduced-motion все узлы встали бы кучей в угол 0 */
@keyframes oc-rotate { from { transform: rotate(0turn); } to { transform: rotate(1turn); } }
@media (prefers-reduced-motion: reduce) { .oc-spin, .oc-keep { animation: none; } }
`}</style>
{ring && (
<div
style={{
position: "absolute",
left: "50%",
top: "50%",
width: radius * 2,
height: radius * 2,
margin: -radius,
borderRadius: "999px",
border: "1px dashed oklch(0.34 0.02 270 / 0.7)",
}}
/>
)}
{items.map((child, i) => (
<div
key={i}
className="oc-spin"
style={
{
transform: `rotate(${i / n}turn)`,
"--oc-dur": `${duration}s`,
"--oc-delay": `${(-duration * i) / n}s`,
"--oc-dir": reverse ? "reverse" : "normal",
"--oc-keep-dir": reverse ? "normal" : "reverse",
"--oc-size": `${iconSize}px`,
} as CSSProperties
}
>
{/* узел выносится на орбиту трансформом РОДИТЕЛЯ-вращателя: translate до и после
rotate складываются в круговую траекторию без единого вычисления угла */}
<div style={{ transform: `translateX(${radius}px)` }}>
<div className="oc-keep" style={{ transform: `rotate(${-i / n}turn)` }}>
{child}
</div>
</div>
</div>
))}
</div>
);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — Magic UI Orbiting Circles (MIT)v1.0.0