← каталог
Split Reveal
Заголовок, где каждое слово выезжает из-под маски со стаггером — классический вход текста.
gsaploadrevealstaggerintermediate~23 КБreduced-motion ✓
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| text? | string | "Премиум-анимации, готовые к копированию… |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/split-reveal-text.jsonзависимости: gsap@^3.15.0 · @gsap/react@^2.1.2
Что нужно в проекте до установки
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 { useRef } from "react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
/** Текст, где каждое слово выезжает из-под маски со стаггером (GSAP). data-shot/reduced → финальное состояние. */
export function SplitReveal({ text = "Премиум-анимации, готовые к копированию" }: { text?: string }) {
const ref = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const el = ref.current;
if (!el) return;
const words = el.querySelectorAll(".sr-word");
if (
document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
gsap.set(words, { yPercent: 0, opacity: 1 });
return;
}
gsap.from(words, { yPercent: 115, opacity: 0, duration: 0.8, ease: "power3.out", stagger: 0.08 });
},
{ scope: ref },
);
return (
<div
ref={ref}
style={{ fontSize: "2rem", fontWeight: 700, lineHeight: 1.15, textAlign: "center", maxWidth: 460, color: "oklch(0.94 0.01 265)" }}
>
{text.split(" ").map((w, i) => (
<span key={i} style={{ display: "inline-block", overflow: "hidden", verticalAlign: "bottom" }}>
<span className="sr-word" style={{ display: "inline-block" }}>
{w}
</span>
</span>
))}
</div>
);
}
import gsap from "gsap";
/**
* Split Reveal — vanilla JS/GSAP (без React). Слова элемента выезжают из-под масок
* со стаггером при появлении во вьюпорте (split делается автоматически).
*
* <h2 id="title">Премиум-анимации, готовые к копированию</h2>
* <script type="module">
* import { initSplitReveal } from './split-reveal-text.vanilla.js';
* const destroy = initSplitReveal(document.querySelector('#title'));
* </script>
*
* @param {HTMLElement} el — текстовый элемент (контент пересобирается в span-маски)
* @param {{ stagger?: number, once?: boolean }} [options]
* @returns {() => void} destroy (возвращает исходный текст)
*/
export function initSplitReveal(el, { stagger = 0.08, once = true } = {}) {
const original = el.textContent ?? "";
const frozen =
document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (frozen) return () => {};
el.textContent = "";
const words = original.split(/\s+/).map((w) => {
const mask = document.createElement("span");
mask.style.cssText = "display:inline-block;overflow:hidden;vertical-align:bottom;";
const word = document.createElement("span");
word.style.display = "inline-block";
word.textContent = `${w} `;
mask.appendChild(word);
el.appendChild(mask);
return word;
});
let tween = null;
const io = new IntersectionObserver(
([e]) => {
if (!e?.isIntersecting) return;
if (once) io.disconnect();
tween = gsap.from(words, { yPercent: 115, opacity: 0, duration: 0.8, ease: "power3.out", stagger });
},
{ threshold: 0.4 },
);
io.observe(el);
return () => {
io.disconnect();
tween?.kill();
el.textContent = original;
};
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0