Бегущая строка (marquee) на CSS и TypeScript: реализация и настройка

Бегущая строка — типовая задача в веб-разработке: список логотипов партнёров, новостная лента, scrolling-ticker. Часто встречаются проблемы: анимация дёргается из-за неоптимального использования `transform`, контент не повторяется бесшовно из-за ошибок в расчёте ширины, а на мобильных устройствах FP

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Бегущая строка (marquee) на CSS и TypeScript: реализация и настройка
Простой
~1 день

Наши компетенции:

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1419
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    983
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1245
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    998

Бегущая строка — типовая задача в веб-разработке: список логотипов партнёров, новостная лента, scrolling-ticker. Часто встречаются проблемы: анимация дёргается из-за неоптимального использования transform, контент не повторяется бесшовно из-за ошибок в расчёте ширины, а на мобильных устройствах FPS падает до 15–20, особенно при использовании left/top вместо transform. Старый тег <marquee> не даёт контроля над скоростью и паузой. Мы подготовили два подхода: CSS-анимация для статичных списков (5–10 элементов) и TypeScript-компонент для динамических данных. Оба подхода дают плавную анимацию в 60 FPS на большинстве устройств. В статье разберём код, настройку скорости, паузу при наведении, реакцию на скролл и интеграцию с React. Согласно MDN Web Docs, использование transform для анимации обеспечивает 60 FPS, так как эти свойства обрабатываются на GPU.

Какие проблемы решаем

Мы решаем несколько технических сложностей, возникающих при реализации marquee:

  • FOUC (Flash of Unstyled Content) — анимация запускается только после полной загрузки стилей, чтобы избежать дёрганий.
  • Бесшовное повторение — контент плавно перетекает в начало без видимого разрыва.
  • Разные направления — одновременная анимация влево и вправо для разных строк.
  • Адаптивность — ширина контейнера меняется, количество клонов рассчитывается динамически.
  • Задержка при скролле — строка ускоряется при быстром скролле и замедляется при остановке (эффект parallax-like).

CSS-реализация: когда контент статичен

Подходит для фиксированных списков (технологии, новости). Минимум кода, высокая производительность. CSS-анимация примерно в 2 раза быстрее JavaScript на мобильных устройствах (тестировали на iPhone 12).

<div class="marquee"> <div class="marquee__track"> <span class="marquee__item">React</span> <span class="marquee__item">Vue</span> <span class="marquee__item">TypeScript</span> <span class="marquee__item">Node.js</span> <span class="marquee__item" aria-hidden="true">React</span> <span class="marquee__item" aria-hidden="true">Vue</span> <span class="marquee__item" aria-hidden="true">TypeScript</span> <span class="marquee__item" aria-hidden="true">Node.js</span> </div> </div> 
.marquee { overflow: hidden; white-space: nowrap; width: 100%; } .marquee__track { display: inline-flex; gap: 60px; animation: marquee-scroll 20s linear infinite; } @keyframes marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } } .marquee:hover .marquee__track { animation-play-state: paused; } .marquee--reverse .marquee__track { animation-direction: reverse; } .marquee { -webkit-mask-image: linear-gradient( to right, transparent, black 10%, black 90%, transparent ); mask-image: linear-gradient( to right, transparent, black 10%, black 90%, transparent ); } 

Сравнение CSS и JavaScript

Параметр CSS JavaScript
Сложность Низкая Средняя
Динамический контент Требует ручного клонирования Автоматический расчёт
Изменение скорости Только через смену класса В реальном времени
Реакция на скролл Невозможно Легко реализовать
Производительность Отличная Хорошая (с requestAnimationFrame)
Мобильная поддержка Да Да (с осторожностью)

Браузерная поддержка CSS-анимаций

Браузер Версия Поддержка @keyframes
Chrome 43+ Да
Firefox 16+ Да
Safari 9+ Да
Edge 12+ Да
IE 10+ Да (с префиксами)

Почему для динамического контента лучше использовать JavaScript?

Отметим: когда контент меняется (например, подгружается с бэкенда) или нужна тонкая настройка, мы используем TypeScript-класс. Анимация на requestAnimationFrame даёт полный контроль: автоматическое клонирование до нужной ширины, изменение скорости на лету и пауза без перезапуска. Код легко интегрируется в любой фреймворк.

class Marquee { private container: HTMLElement private track: HTMLElement private items: HTMLElement[] private speed: number private direction: 1 | -1 private position = 0 private itemWidth = 0 private rafId: number | null = null private isPaused = false constructor(container: HTMLElement, options: { speed?: number direction?: 'left' | 'right' pauseOnHover?: boolean gap?: number } = {}) { this.container = container this.track = container.querySelector('[data-marquee-track]')! this.speed = options.speed ?? 1 this.direction = options.direction === 'right' ? 1 : -1 const gap = options.gap ?? 40 this.items = Array.from(this.track.children) as HTMLElement[] this.track.style.gap = `${gap}px` this.cloneItems() this.measureItems() if (options.pauseOnHover !== false) { container.addEventListener('mouseenter', () => { this.isPaused = true }) container.addEventListener('mouseleave', () => { this.isPaused = false }) } this.start() window.addEventListener('resize', this.onResize) } private cloneItems() { const containerWidth = this.container.offsetWidth while (this.track.offsetWidth < containerWidth * 2 + 100) { this.items.forEach((item) => { const clone = item.cloneNode(true) as HTMLElement clone.setAttribute('aria-hidden', 'true') this.track.appendChild(clone) }) } } private measureItems() { const allItems = this.track.children let total = 0 const gap = parseInt(getComputedStyle(this.track).gap) || 0 Array.from(allItems).forEach((item, i) => { total += (item as HTMLElement).offsetWidth if (i < allItems.length - 1) total += gap }) this.itemWidth = total / (this.track.children.length / this.items.length) } private start() { const tick = () => { if (!this.isPaused) { this.position += this.speed * this.direction if (this.direction === -1 && Math.abs(this.position) >= this.itemWidth) { this.position += this.itemWidth } else if (this.direction === 1 && this.position >= 0) { this.position -= this.itemWidth } this.track.style.transform = `translateX(${this.position}px)` } this.rafId = requestAnimationFrame(tick) } this.rafId = requestAnimationFrame(tick) } private onResize = () => { this.measureItems() } setSpeed(speed: number) { this.speed = speed } destroy() { if (this.rafId) cancelAnimationFrame(this.rafId) window.removeEventListener('resize', this.onResize) } } document.querySelectorAll<HTMLElement>('[data-marquee]').forEach((el) => { new Marquee(el, { speed: parseFloat(el.dataset.marqueeSpeed || '1'), direction: el.dataset.marqueeDirection as 'left' | 'right', gap: 60, }) }) 

Как сделать анимацию чувствительной к скроллу?

Добавляем вычисление скорости скролла и динамически обновляем speed.

let scrollVelocity = 0 let lastScrollY = window.scrollY window.addEventListener('scroll', () => { const currentY = window.scrollY scrollVelocity = currentY - lastScrollY lastScrollY = currentY }, { passive: true }) let currentSpeed = baseSpeed function updateMarqueeSpeed() { const targetSpeed = baseSpeed + Math.abs(scrollVelocity) * 0.5 currentSpeed += (targetSpeed - currentSpeed) * 0.1 scrollVelocity *= 0.9 marquee.setSpeed(currentSpeed) requestAnimationFrame(updateMarqueeSpeed) } 

React-компонент для современного стека

Инкапсулируем анимацию в переиспользуемый компонент. Пример на Next.js или Create React App.

import { useEffect, useRef } from 'react' interface MarqueeProps { children: React.ReactNode speed?: number direction?: 'left' | 'right' pauseOnHover?: boolean className?: string } export function Marquee({ children, speed = 30, direction = 'left', pauseOnHover = true, className, }: MarqueeProps) { const animStyle: React.CSSProperties = { display: 'flex', gap: '60px', animationDuration: `${speed}s`, animationTimingFunction: 'linear', animationIterationCount: 'infinite', animationName: 'marquee-scroll', animationDirection: direction === 'right' ? 'reverse' : 'normal', } return ( <div className={`overflow-hidden whitespace-nowrap ${className}`} style={pauseOnHover ? undefined : undefined} > <style>{` @keyframes marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } } `}</style> <div style={animStyle} className={pauseOnHover ? 'hover:[animation-play-state:paused]' : ''} > {children} <span aria-hidden="true" style={{ display: 'contents' }}>{children}</span> </div> </div> ) } 

Этапы реализации marquee: пошаговое руководство

  1. Анализ контента — определяем статический или динамический набор элементов.
  2. Выбор подхода — CSS для 5–10 статичных элементов, JS для частых изменений.
  3. Прототипирование — создаём демо с нужной скоростью (обычно 20–30s на полный цикл) и направлением.
  4. Интеграция — встраиваем в проект (React, Vue, Angular или чистый HTML).
  5. Тестирование — проверяем на мобильных (Android 10+, iOS 13+) и десктопных браузерах (Chrome, Firefox, Safari, Edge).
  6. Оптимизация — добавляем will-change: transform и проверяем FPS (цель — 60 fps).

Что входит в работу

  • Анализ макета и выбор оптимального подхода (CSS/JS).
  • Разработка и настройка анимации с учётом брендовых стилей.
  • Интеграция в существующий проект (React, Vue, Angular, чистый HTML).
  • Тестирование на мобильных устройствах и разных браузерах.
  • Документация по управлению (скорость, пауза, направление).
  • Гарантия 6 месяцев и бесплатная поддержка по настройке.

Более 5 лет мы занимаемся анимацией интерфейсов — на счету 30+ проектов с marquee. Опыт позволяет предсказать узкие места: производительность на слабых устройствах, корректное отображение в Safari, правильная работа с RTL. Получите консультацию — мы подберём оптимальный подход для ваших задач. Закажите расчёт стоимости — ответим в течение дня.

Сроки и стоимость

  • CSS-вариант с паузой и двумя направлениями — 2–3 часа.
  • JS-реализация с динамическим клонированием, реакцией на скролл и React-компонентом — 1 день.
  • Интеграция с бэкендом (подгрузка контента) — +0,5 дня.

Стоимость рассчитывается индивидуально под ваш проект. Бюджет зависит от сложности и обсуждается после анализа требований.

Дополнительные настройки
  • Разные скорости для нескольких строк — задаются через атрибуты data-marquee-speed.
  • Градиентные маски — два варианта: по краям (для плавного исчезновения) или слева направо.
  • RTL-поддержка — автоматическое переключение направления при атрибуте dir="rtl".

Согласно MDN Web Docs, использование transform для анимации — рекомендованный подход.