← каталог
Rotating Words
Ротатор слов: рамка обжимается по ширине текущего слова, слово уезжает вверх, следующее приходит снизу. Строка не прыгает — ширина берётся у скрытого эталона.
cssautorevealbasic~2 КБreduced-motion ✓
🎛 Настройки — крути и копируй
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| prefix? | string | "делаем" | Постоянная часть строки. |
| words? | string[] | WORDS | Слова, которые сменяют друг друга. |
| hold? | number | 1900 | Сколько слово держится на экране, мс (формат «1900»). |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/rotating-words.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.
Экспорт
Код
"use client";
import { useEffect, useRef, useState } from "react";
export interface RotatingWordsProps {
/** Постоянная часть строки. */
prefix?: string;
/** Слова, которые сменяют друг друга. */
words?: string[];
/** Сколько слово держится на экране, мс (формат «1900»). */
hold?: number;
}
const WORDS = ["интерфейсы", "лендинги", "витрины", "порталы"];
/**
* Ротатор слов: рамка обжимается по ширине текущего слова, само слово уезжает вверх,
* следующее приходит снизу. Ширина анимируется через inline-size конкретного значения
* (меряем скрытый эталон), поэтому строка не «прыгает» и текст рядом не дёргается.
* `prefers-reduced-motion` — слова просто сменяются без движения.
*/
export function RotatingWords({ prefix = "делаем", words = WORDS, hold = 1900 }: RotatingWordsProps) {
const [i, setI] = useState(0);
const [width, setWidth] = useState<number | null>(null);
const measure = useRef<HTMLSpanElement>(null);
useEffect(() => {
const frozen =
document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (frozen) return;
const id = setInterval(() => setI((v) => (v + 1) % words.length), hold);
return () => clearInterval(id);
}, [words.length, hold]);
// ширину берём у скрытого эталона того же шрифта — без чтения layout в анимации
useEffect(() => {
const el = measure.current?.children[i] as HTMLElement | undefined;
if (el) setWidth(el.offsetWidth);
}, [i, words]);
return (
<span className="rtw">
<style>{`
.rtw { display: inline-flex; align-items: baseline; gap: 0.32em; font-weight: 700;
letter-spacing: -0.03em; }
.rtw-slot { position: relative; display: inline-block; overflow: hidden;
vertical-align: bottom; height: 1.15em;
transition: width 0.45s var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)); }
.rtw-word { position: absolute; left: 0; top: 0; white-space: nowrap;
background: linear-gradient(92deg, oklch(0.8 0.16 300), oklch(0.78 0.15 220));
-webkit-background-clip: text; background-clip: text; color: transparent;
animation: rtw-in 0.5s var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
@keyframes rtw-in { from { transform: translateY(105%); opacity: 0; } }
.rtw-measure { position: absolute; visibility: hidden; pointer-events: none;
white-space: nowrap; height: 0; overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
.rtw-slot { transition: none; } .rtw-word { animation: none; }
}
`}</style>
<span>{prefix}</span>
<span className="rtw-slot" style={{ width: width ? `${width}px` : "auto" }}>
<span key={i} className="rtw-word">
{words[i]}
</span>
</span>
<span ref={measure} className="rtw-measure" aria-hidden>
{words.map((w) => (
<span key={w}>{w}</span>
))}
</span>
</span>
);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0