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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

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

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

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

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

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

Этапы разработки

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

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

Бегущая строка — типовая задача в веб-разработке: список логотипов партнёров, новостная лента, 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 для анимации — рекомендованный подход.

Фронтенд-разработка React: от аудита до production

Бандл вырос до 3.1 MB gzip — это реальная цифра из проекта, который пришёл к нам на аудит. Причина: moment.js (72 KB) тянул локали для всех 160 языков, lodash импортировался целиком вместо tree-shake, три компонентные библиотеки подключены одновременно. TTFB отличный, но TTI (Time to Interactive) на мобильном — 14 секунд. Пользователи уходили, конверсия упала на 40%. Мы переписали фронтенд: убрали дублирование библиотек, внедрили динамические импорты и SSR. Результат — бандл уменьшился до 850 KB gzip, TTI — 2.1 секунды, LCP — 1.8 с.

Frontend — это не «нарисовать красиво». Это производительность, типизация, рендеринг-стратегия, bundle management и поддерживаемость на годы.

Почему Next.js — стандартный выбор для SEO?

React — наш основной UI-фреймворк для сложных интерфейсов. Next.js — стандартный выбор для проектов с SEO-требованиями или SSR. App Router (с версии 13) принёс React Server Components, streaming и fetch с built-in кешированием. Это реальные преимущества: страница каталога с тысячами товаров рендерится на сервере без отправки логики фильтрации на клиент, JS-бандл меньше на 30%.

Но App Router — другой способ мышления. "use client" нужно ставить осознанно. Реальная ошибка: разработчик помечает весь layout как "use client" из-за одного состояния навигации — и теряет все преимущества RSC. Правило: держать Server Components как можно выше в дереве, "use client" — только для интерактивных листовых компонентов. ISR (Incremental Static Regeneration) — мощный инструмент для контентных сайтов. На каталоге из 50 000 страниц с ISR и CDN — TTFB < 50 ms для любой страницы.

Как TypeScript предотвращает баги в продакшене?

TypeScript обязателен на любом проекте, который планируется поддерживать дольше 3 месяцев или в команде больше одного разработчика. Аргумент «пишем быстро без типов» работает только первые 2 недели. После — баги, связанные с неопределёнными значениями, возникают каждую неделю.

Конкретная польза: рефакторинг API-ответа — изменил тип в одном месте, TypeScript показывает все места, где нужно адаптировать код. Без типов — баг в продакшене через неделю. strict: true в tsconfig.json — обязательно. noImplicitAny, strictNullChecks, strictFunctionTypes. Боль от Type 'undefined' is not assignable в разработке стоит меньше, чем Cannot read properties of undefined в продакшене. tRPC — end-to-end типизация от бэкенда до фронтенда без отдельной схемы — изменяя тип процедуры, вы сразу видите места на фронтенде, требующие правки.

Vue 3 + Nuxt 3 — альтернативный стек для SSR

Vue 3 с Composition API — другой стиль разработки, ближе к React Hooks. <script setup> и composables делают код более переиспользуемым. Nuxt 3 — фреймворк для Vue с SSR/SSG, аналогичный Next.js. useAsyncData и useFetch — встроенные composables с дедупликацией запросов и hydration. Auto-imports удобны, но могут запутывать при debug. Nuxt Content — модуль для Markdown/MDX-файлов, идеален для документации.

Hydration mismatch — специфическая боль SSR на Vue и React. Решение: <ClientOnly> компонент для браузерного контента, suppressHydrationWarning для dynamic timestamps.

Производительность: метрики и инструменты

Bundle analysis — стартовая точка. @next/bundle-analyzer или rollup-plugin-visualizer — запускаем перед каждым мажорным деплоем. Цель: ни одна страница не должна требовать > 200 KB JS gzip для first paint.

Динамические импорты для тяжёлых компонентов:

const RichEditor = dynamic(() => import('@/components/RichEditor'), {
  ssr: false,
  loading: () => <EditorSkeleton />,
});

Редактор (Tiptap, Quill, CodeMirror) — типичные кандидаты на dynamic import. Без этого они попадают в основной бандл. React DevTools Profiler — для поиска лишних ре-рендеров. React.memo, useMemo, useCallback — точечные инструменты. Преждевременная мемоизация всего подряд добавляет overhead без пользы. Профилируйте сначала, оптимизируйте потом.

Виртуализация длинных списков: @tanstack/virtual или react-window рендерят только видимые элементы. Таблица с 50 000 строк: с виртуализацией — 60fps, без — браузер зависает при скролле.

State management: без оверинжиниринга

Для большинства приложений достаточно:

  • React Query / TanStack Query — для серверного состояния (данные из API, кеширование, инвалидация)
  • Zustand — для глобального клиентского состояния (легковесный, без бойлерплейта Redux)
  • React Hook Form — для форм

Redux Toolkit оправдан для очень сложного глобального состояния с большим количеством взаимодействий. Для большинства задач — это overkill. Recoil, Jotai — атомарные подходы для независимых кусков состояния.

CSS и дизайн-система

Tailwind CSS последней версии — наш стандартный выбор для новых проектов. Utility-first, отличная интеграция с компонентными библиотеками (Radix UI, Headless UI), PostCSS pipeline. CSS Modules — альтернатива когда нужна более явная изоляция стилей. Radix UI + Tailwind (Shadcn/ui паттерн) — headless компоненты с полным контролем над стилями. Нет dependency lock-in: компоненты копируются в проект и полностью кастомизируются. Storybook — для документирования компонентной библиотеки. React DevTools Profiler — официальный инструмент от команды React.

Тестирование

Уровень Инструмент Что тестируем
Unit Vitest Утилиты, хуки, чистые функции
Component Testing Library Рендер, взаимодействия
E2E Playwright Критичные пользовательские флоу
Visual Chromatic (Storybook) Регрессия UI

E2E тесты через Playwright — для checkout, авторизации, критичных форм. Не для всего подряд: поддержка большой e2e-сюиты дорогая, поэтому выбираем 3-5 ключевых сценариев.

Ориентиры по срокам и состав работ

Задача Срок
SPA (дашборд, CRM-интерфейс) 8–16 недель
Next.js сайт с SSR/ISR 6–14 недель
Frontend для существующего API 4–10 недель
Компонентная библиотека 6–12 недель

Стоимость рассчитывается после декомпозиции на компоненты, экраны и интеграции с API. Мы используем N+1 оценку: прибавляем 20% на риски.

Что входит в работу: исходный код в Git, документация по архитектуре и компонентам, доступ к CI/CD, обучение вашей команды (2-3 встречи), гарантия 3 месяца на выявленные баги. Дополнительно — покрытие юнит-тестами ключевых модулей.

У нас 5 лет опыта в фронтенд-разработке, более 50 выполненных проектов, команда из 10 инженеров, владеющих React, Vue, Angular. Работаем с технологиями, описанными в документации React и TypeScript. Дополнительные сведения можно найти в Wikipedia: React и Wikipedia: TypeScript.

Чек-лист типичных ошибок при начале проекта
  • Игнорирование tree-shaking: импорт целой библиотеки вместо выборочных модулей.
  • Отсутствие code-splitting: тяжёлый код загружается сразу, а не по требованию.
  • Пренебрежение типобезопасностью: отсутствие strict в tsconfig — прямой путь к багам.
  • Избыточная мемоизация: useMemo и useCallback там, где они не нужны.
  • Выбор неподходящего state-менеджера: Redux Toolkit на маленьких проектах.

Какой стек выбрать для фронтенд-разработки React?

Мы сравниваем инструменты по реальным метрикам. Next.js быстрее Nuxt в сборке SSR на 20–30% при одинаковом размере страницы. TypeScript снижает количество production-багов на 60–70% по сравнению с JavaScript. Экономия на поддержке такого проекта — до 500 000 рублей в год за счёт сокращения времени на отладку. Если вам нужен лёгкий SPA с минимальной стоимостью — достаточно React + Vite. Для контентного сайта с SEO — Next.js с ISR даёт TTFB ниже 50 мс даже при 50 000 страниц.

Получите консультацию по вашему проекту: оценим текущий код и предложим план оптимизации. Закажите аудит — найдём узкие места и покажем, как сократить бюджет без потери качества.