Перейти к содержимому

Интерфейс

Цикличный текст

Анимированный компонент для цикличного переключения текста с использованием GSAP.


Цикличный текст

Компонент для плавной анимированной смены текста. Идеально для 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">&nbsp;</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">&nbsp;</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 gsap
npm install gsap