Бігучий рядок (marquee) на CSS та TypeScript: реалізація та налаштування

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

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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
    1246
  • 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 для анімації — рекомендований підхід.