Marquee
Бесконечно прокручиваемая бегущая строка с поддержкой любого контента. Использует GSAP для плавной анимации и реагирует на скорость прокрутки страницы.
Интерактивное демо
Дизайн Разработка Анимации Интерфейсы
Дизайн Разработка Анимации Интерфейсы
Дизайн Разработка Анимации Интерфейсы
Пропсы
8
32
<script lang="ts">
import { onMount } from "svelte";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { registerPluginOnce } from "$lib/helpers/gsap";
import type { Snippet } from "svelte";
import { cn } from "$lib/utils/cn";
type Props = {
class?: string;
gap?: number;
children?: Snippet;
repeat?: number;
duration?: number;
velocity?: number;
reversed?: boolean;
scrollElement?: string | HTMLElement | null;
};
let {
class: className = "",
gap = 32,
children,
repeat = 3,
duration = 5,
velocity = 0.5,
reversed = false,
scrollElement,
}: Props = $props();
let container = $state<HTMLElement>();
const attachContainer = (node: HTMLElement) => {
container = node;
return () => {
if (container === node) {
container = undefined;
}
};
};
onMount(() => {
registerPluginOnce(ScrollTrigger);
const parts = container?.querySelectorAll(".marquee-part");
if (!parts?.length) return;
const resolvedScroller =
typeof scrollElement === "string"
? document.querySelector<HTMLElement>(scrollElement)
: scrollElement instanceof HTMLElement
? scrollElement
: null;
const scroller =
resolvedScroller instanceof HTMLElement ? resolvedScroller : window;
let direction = reversed ? -1 : 1;
let timeline: gsap.core.Timeline | null = null;
const ctx = gsap.context(() => {
timeline = gsap.timeline({
repeat: -1,
onReverseComplete(this: gsap.core.Timeline) {
this.totalTime(this.rawTime() + this.duration() * 10);
},
});
timeline.to(parts, {
xPercent: -100,
ease: "none",
duration,
});
if (reversed) {
timeline.progress(1);
timeline.timeScale(-1);
}
ScrollTrigger.create({
scroller,
onUpdate(self) {
if (!timeline) return;
const currentScrollDir = self.direction;
const targetDir = reversed ? -currentScrollDir : currentScrollDir;
if (direction !== targetDir) {
direction = targetDir;
gsap.to(timeline, { timeScale: direction, overwrite: true });
}
const scrollVel = self.getVelocity();
if (Math.abs(scrollVel) > 0) {
const timeScale =
direction * (1 + Math.abs(scrollVel * velocity) / 1000);
gsap.to(timeline, { timeScale, overwrite: true, duration: 0.1 });
gsap.to(timeline, {
timeScale: direction,
duration: 0.5,
delay: 0.1,
overwrite: "auto",
});
}
},
});
}, container);
return () => {
ctx.revert();
timeline = null;
};
});
</script>
<div {@attach attachContainer} class={cn("flex h-full w-full", className)}>
{#each Array(repeat) as _, i (i)}
<div
class="marquee-part flex shrink-0"
style:gap="{gap}px"
style:padding-left="{gap / 2}px"
style:padding-right="{gap / 2}px"
aria-hidden={i > 0}
>
{@render children?.()}
</div>
{/each}
</div><script lang="ts">
import { onMount } from "svelte";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { registerPluginOnce } from "$lib/helpers/gsap";
import type { Snippet } from "svelte";
import { cn } from "$lib/utils/cn";
type Props = {
class?: string;
gap?: number;
children?: Snippet;
repeat?: number;
duration?: number;
velocity?: number;
reversed?: boolean;
scrollElement?: string | HTMLElement | null;
};
let {
class: className = "",
gap = 32,
children,
repeat = 3,
duration = 5,
velocity = 0.5,
reversed = false,
scrollElement,
}: Props = $props();
let container = $state<HTMLElement>();
const attachContainer = (node: HTMLElement) => {
container = node;
return () => {
if (container === node) {
container = undefined;
}
};
};
onMount(() => {
registerPluginOnce(ScrollTrigger);
const parts = container?.querySelectorAll(".marquee-part");
if (!parts?.length) return;
const resolvedScroller =
typeof scrollElement === "string"
? document.querySelector<HTMLElement>(scrollElement)
: scrollElement instanceof HTMLElement
? scrollElement
: null;
const scroller =
resolvedScroller instanceof HTMLElement ? resolvedScroller : window;
let direction = reversed ? -1 : 1;
let timeline: gsap.core.Timeline | null = null;
const ctx = gsap.context(() => {
timeline = gsap.timeline({
repeat: -1,
onReverseComplete(this: gsap.core.Timeline) {
this.totalTime(this.rawTime() + this.duration() * 10);
},
});
timeline.to(parts, {
xPercent: -100,
ease: "none",
duration,
});
if (reversed) {
timeline.progress(1);
timeline.timeScale(-1);
}
ScrollTrigger.create({
scroller,
onUpdate(self) {
if (!timeline) return;
const currentScrollDir = self.direction;
const targetDir = reversed ? -currentScrollDir : currentScrollDir;
if (direction !== targetDir) {
direction = targetDir;
gsap.to(timeline, { timeScale: direction, overwrite: true });
}
const scrollVel = self.getVelocity();
if (Math.abs(scrollVel) > 0) {
const timeScale =
direction * (1 + Math.abs(scrollVel * velocity) / 1000);
gsap.to(timeline, { timeScale, overwrite: true, duration: 0.1 });
gsap.to(timeline, {
timeScale: direction,
duration: 0.5,
delay: 0.1,
overwrite: "auto",
});
}
},
});
}, container);
return () => {
ctx.revert();
timeline = null;
};
});
</script>
<div {@attach attachContainer} class={cn("flex h-full w-full", className)}>
{#each Array(repeat) as _, i (i)}
<div
class="marquee-part flex shrink-0"
style:gap="{gap}px"
style:padding-left="{gap / 2}px"
style:padding-right="{gap / 2}px"
aria-hidden={i > 0}
>
{@render children?.()}
</div>
{/each}
</div>Использование
Базовая бегущая строка
<Marquee>
<span>Пункт 1</span>
<span>Пункт 2</span>
<span>Пункт 3</span>
</Marquee><Marquee>
<span>Пункт 1</span>
<span>Пункт 2</span>
<span>Пункт 3</span>
</Marquee>С изображениями
<Marquee gap={48} duration={10}>
<img src="/logo1.svg" alt="Logo 1" class="h-12" />
<img src="/logo2.svg" alt="Logo 2" class="h-12" />
<img src="/logo3.svg" alt="Logo 3" class="h-12" />
</Marquee><Marquee gap={48} duration={10}>
<img src="/logo1.svg" alt="Logo 1" class="h-12" />
<img src="/logo2.svg" alt="Logo 2" class="h-12" />
<img src="/logo3.svg" alt="Logo 3" class="h-12" />
</Marquee>Обратное направление
<Marquee reversed={true}>
<span>Прокрутка справа налево</span>
</Marquee><Marquee reversed={true}>
<span>Прокрутка справа налево</span>
</Marquee>Пропсы
| Пропс | Тип | По умолчанию | Описание |
|---|---|---|---|
duration | number | 5 | Длительность одного цикла в секундах |
gap | number | 32 | Расстояние между элементами в пикселях |
repeat | number | 3 | Количество повторений контента |
reversed | boolean | false | Обратное направление прокрутки |
velocity | number | 0.5 | Множитель скорости от прокрутки |
scrollElement | string HTMLElement null | — | Элемент для отслеживания прокрутки |
class | string | — | Дополнительные классы |
