Цикличный текст
Компонент для плавной анимированной смены текста. Идеально для hero-секций, заголовков и акцентных элементов. Использует GSAP: эффект blur + вертикальное смещение с плавным transition ширины контейнера.
Превью
Проект про знания
<script lang="ts">
import { onMount } from "svelte";
import { gsap } from "gsap";
import { cn } from "../../utils/cn";
type Props = {
/**
* Array of text strings to loop through.
*/
texts: string[];
/**
* Interval in milliseconds between text changes.
* @default 2000
*/
interval?: number;
/**
* Additional CSS classes for the container.
*/
class?: string;
}
let { texts, interval = 2000, class: className }: Props = $props();
let currentIndex = $state(0);
let isFirst = $state(true);
function gsapTransition(
node: HTMLElement,
{ direction }: { direction: "in" | "out" },
) {
gsap.killTweensOf(node);
const parent = node.parentElement;
if (direction === "in") {
if (isFirst) {
gsap.set(node, { yPercent: 0, opacity: 1, filter: "blur(0px)" });
return { duration: 0, tick: function _tick() { /* empty */ } };
}
if (parent) {
gsap.fromTo(
parent,
{ width: parent.offsetWidth },
{ width: node.offsetWidth, duration: 0.35, ease: "power2.inOut" },
);
}
gsap.fromTo(
node,
{ yPercent: 50, opacity: 0, filter: "blur(8px)" },
{
yPercent: 0,
opacity: 1,
filter: "blur(0px)",
duration: 0.3,
delay: 0.25,
ease: "back.out(1.2)",
},
);
return { duration: 900, tick: function _tick() { /* empty */ } };
}
if (parent) parent.style.width = `${String(parent.offsetWidth)}px`;
Object.assign(node.style, {
position: "absolute",
top: "0",
left: "0",
width: "100%",
});
gsap.to(node, {
yPercent: -50,
opacity: 0,
filter: "blur(6px)",
duration: 0.2,
ease: "power2.in",
});
return { duration: 300, tick: function _tick() { /* empty */ } };
}
onMount(() => {
const loopInterval = setInterval(() => {
if (document.hidden) return;
isFirst = false;
currentIndex = (currentIndex + 1) % texts.length;
}, interval);
return () => { clearInterval(loopInterval); };
});
</script>
<span
class={cn("font-inherit relative inline-block text-inherit", className)}
style="clip-path: inset(-100vh 0 -100vh 0);"
>
<span
class="font-inherit invisible inline-block w-0 text-inherit"
aria-hidden="true"> </span
>{#key currentIndex}<span
class={cn(
"font-inherit whitespace-nowrap text-inherit",
isFirst ? "relative inline-block" : "absolute top-0 left-0",
)}
in:gsapTransition={{ direction: "in" }}
out:gsapTransition={{ direction: "out" }}
>
{texts[currentIndex]}
</span>{/key}
</span>
<script lang="ts">
import { onMount } from "svelte";
import { gsap } from "gsap";
import { cn } from "../../utils/cn";
type Props = {
/**
* Array of text strings to loop through.
*/
texts: string[];
/**
* Interval in milliseconds between text changes.
* @default 2000
*/
interval?: number;
/**
* Additional CSS classes for the container.
*/
class?: string;
}
let { texts, interval = 2000, class: className }: Props = $props();
let currentIndex = $state(0);
let isFirst = $state(true);
function gsapTransition(
node: HTMLElement,
{ direction }: { direction: "in" | "out" },
) {
gsap.killTweensOf(node);
const parent = node.parentElement;
if (direction === "in") {
if (isFirst) {
gsap.set(node, { yPercent: 0, opacity: 1, filter: "blur(0px)" });
return { duration: 0, tick: function _tick() { /* empty */ } };
}
if (parent) {
gsap.fromTo(
parent,
{ width: parent.offsetWidth },
{ width: node.offsetWidth, duration: 0.35, ease: "power2.inOut" },
);
}
gsap.fromTo(
node,
{ yPercent: 50, opacity: 0, filter: "blur(8px)" },
{
yPercent: 0,
opacity: 1,
filter: "blur(0px)",
duration: 0.3,
delay: 0.25,
ease: "back.out(1.2)",
},
);
return { duration: 900, tick: function _tick() { /* empty */ } };
}
if (parent) parent.style.width = `${String(parent.offsetWidth)}px`;
Object.assign(node.style, {
position: "absolute",
top: "0",
left: "0",
width: "100%",
});
gsap.to(node, {
yPercent: -50,
opacity: 0,
filter: "blur(6px)",
duration: 0.2,
ease: "power2.in",
});
return { duration: 300, tick: function _tick() { /* empty */ } };
}
onMount(() => {
const loopInterval = setInterval(() => {
if (document.hidden) return;
isFirst = false;
currentIndex = (currentIndex + 1) % texts.length;
}, interval);
return () => { clearInterval(loopInterval); };
});
</script>
<span
class={cn("font-inherit relative inline-block text-inherit", className)}
style="clip-path: inset(-100vh 0 -100vh 0);"
>
<span
class="font-inherit invisible inline-block w-0 text-inherit"
aria-hidden="true"> </span
>{#key currentIndex}<span
class={cn(
"font-inherit whitespace-nowrap text-inherit",
isFirst ? "relative inline-block" : "absolute top-0 left-0",
)}
in:gsapTransition={{ direction: "in" }}
out:gsapTransition={{ direction: "out" }}
>
{texts[currentIndex]}
</span>{/key}
</span>
Текст автоматически меняется каждые 1.8 секунды с плавной анимацией.
Особенности
- Анимация через GSAP (blur + вертикальное смещение)
- Плавное изменение ширины контейнера при смене слова
- Пауза при скрытой вкладке браузера (
document.hidden) - Управляемый интервал смены текста
- Поддержка произвольных CSS-классов (цвет, размер)
Использование
<script>
import TextLoop from '$lib/components/ui/TextLoop.svelte';
</script>
<p>
Мы создаём <TextLoop texts={['сайты', 'приложения', 'инструменты']} class="text-accent" />
</p><script>
import TextLoop from '$lib/components/ui/TextLoop.svelte';
</script>
<p>
Мы создаём <TextLoop texts={['сайты', 'приложения', 'инструменты']} class="text-accent" />
</p>Пропсы
| Пропс | Тип | По умолчанию | Описание |
|---|---|---|---|
texts | string[] | — | Массив строк (обязательный) |
interval | number | 2000 | Интервал смены текста в мс |
class | string | '' | Дополнительные CSS-классы |
Пример с настройками
<TextLoop
texts={['Быстро', 'Качественно', 'Надёжно']}
interval={1500}
class="text-2xl font-bold text-accent"
/><TextLoop
texts={['Быстро', 'Качественно', 'Надёжно']}
interval={1500}
class="text-2xl font-bold text-accent"
/>Зависимости
Требует установленный GSAP:
npm install gsapnpm install gsap