Реалізація Marquee (рядок, що біжить) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

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

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

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Marquee (рядок, що біжить) на сайті
Проста
~1 робочий день
Часті питання

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

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Marquee (бегучої строки) на сайті

Бегуча строка — один із найпростіших ефектів, але з кількома нетривіальними деталями: бесшовне повторення, реакція на швидкість скролу, hover-пауза, різні напрямки для різних рядків.

CSS-реалізація

Чистий CSS без JS — для простих випадків зі фіксованим контентом:

<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%); }
}

/* -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
  );
}

JavaScript-реалізація з динамічним клонуванням

Коли контент динамічний — кількість клонів розраховується автоматично під ширину контейнера:

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() {
    // Клонуємо поки сумарна ширина > ширина контейнера * 2
    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,
  })
})

Реакція на швидкість скролу

Ефект: строка прискорюється при швидкому скролі та сповільнюється при зупинці.

let scrollVelocity = 0
let lastScrollY = window.scrollY

window.addEventListener('scroll', () => {
  const currentY = window.scrollY
  scrollVelocity = currentY - lastScrollY
  lastScrollY = currentY
}, { passive: true })

// У RAF loop маркера — оновлюємо швидкість з damping
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)
}

Вертикальний marquee

Той же принцип, по осі Y:

.marquee--vertical {
  overflow: hidden;
  height: 400px;
}

.marquee--vertical .marquee__track {
  display: flex;
  flex-direction: column;
  gap: 20px;
  animation: marquee-vertical 15s linear infinite;
}

@keyframes marquee-vertical {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}

React-компонент

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>
  )
}

Терміни

CSS-варіант з паузою та двома напрямками — 2–3 години. JS-реалізація з динамічним клонуванням, реакцією на скролл та React-компонентом — 1 день.