← Back to gallery
CSS

Axis-aware Scroll-linked Marquee Ticker

Scroll-linked marquee ticker that documents axis ownership (X vs. Y), duplicate-track accessibility (aria-hidden on the dupe), and a static reduced-motion state. Three variants: horizontal news, vertical stats, and a paired axis-cross layout.

marqueetickerscroll-linkedduplicate-trackaxis-awarearia-hiddenprefers-reduced-motion

Scroll-linked · axis-aware ticker

Axis-aware Scroll-linked Marquee Ticker

Three axis bindings for a scroll-linked ticker — Inline (horizontal track tied to inline scroll progress), Block (a vertical track tied to block scroll progress), and Cross-axis Pair (both rails simultaneously) — using animation-timeline: scroll(...) so the rail advances by user scroll, not by clock time, with a timed fallback when scroll-driven animations are unsupported.

X-axis · inline scroll progress

Inline Axis

A single horizontal rail bound to the inline scroll axis. The track animates from translateX(0) to translateX(-50%) over the full inline scroll range, so as the user scrolls the page horizontally (or, when the page has none, the timed fallback kicks in) the ticker advances one authored track width.

  • scroll(nearest inline)
  • horizontal track
  • X-axis

Y-axis · block scroll progress

Block Axis

A single VERTICAL rail bound to the block scroll axis. The chips travel top-to-bottom so the same translateY keyframe reads as a vertical ticker. As the page scrolls, the rail advances one authored track height — a sidebar / progress-rail pattern, not a header ribbon.

  • scroll(nearest block)
  • vertical track
  • Y-axis

X+Y · two axes side by side

Cross-axis Pair

A horizontal rail (inline-bound) and a vertical rail (block-bound) running side-by-side so the contrast between the two axes reads at a glance. The horizontal track advances on scrollX, the vertical track advances on scrollY — they are scroll-driven independently, never sharing a clock.

  • inline + block
  • two axes
  • visual contrast

Axis-aware ticker inspector

Inline Axis

  • scroll(nearest inline)
  • horizontal track
  • X-axis

A single horizontal rail bound to the inline scroll axis. The track animates from translateX(0) to translateX(-50%) over the full inline scroll range, so as the user scrolls the page horizontally (or, when the page has none, the timed fallback kicks in) the ticker advances one authored track width.

Helped you ship something? 🐟 Send my cat a churu

/* Single horizontal rail; animation-timeline: scroll(nearest inline) binds progress to scrollX, with a timed translateX fallback for unsupported UAs. */
.ticker-viewport {
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.ticker-track {
  display: inline-flex;
  gap: 1rem;
  white-space: nowrap;
  width: max-content;
  /* Timed fallback */
  animation: tickerShiftX 14.00s linear infinite;
}

@supports (animation-timeline: scroll()) {
  .ticker-track {
    /* Scroll-driven override — no clock time involved */
    animation-timeline: scroll(nearest inline);
    animation-range: 0 100%;
    animation-iteration-count: 1;
    animation-duration: auto;
  }
}

@keyframes tickerShiftX {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .ticker-track { animation: none; flex-wrap: wrap; }
}

How to make this

An axis-aware scroll-linked ticker uses duplicated visual rail content, a transform keyframe, and animation-timeline: scroll(nearest inline or block) so scroll progress drives the marquee.

html
<section class="axis-ticker">2  <p class="axis-ticker__summary">    Updates: axis-aware, scroll-driven, inline owner, duplicate rail.  </p>5  <div class="axis-ticker__viewport" aria-hidden="true">6    <div class="axis-ticker__track">      <span>axis-aware</span><span>scroll-driven</span>      <span>inline owner</span><span>duplicate rail</span>      <span>axis-aware</span><span>scroll-driven</span>      <span>inline owner</span><span>duplicate rail</span>    </div>  </div></section> <style>.axis-ticker {  width: min(100%, 420px);  padding: 1rem;  background: #0f1a2d;  color: #e2e8f0;}.axis-ticker__summary {  position: absolute;  width: 1px;  height: 1px;  overflow: hidden;  clip-path: inset(50%);}.axis-ticker__viewport {  overflow: hidden;  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);}.axis-ticker__track {  display: inline-flex;  gap: 1rem;  white-space: nowrap;  width: max-content;38  animation: axis-ticker-inline 14s linear infinite;}@supports (animation-timeline: scroll()) {  .axis-ticker__track {42    animation-timeline: scroll(nearest inline);    animation-range: 0 100%;    animation-duration: auto;    animation-iteration-count: 1;  }}.axis-ticker__track span {  padding: .45rem .75rem;  border-radius: 999px;  background: rgba(255,255,255,.07);}@keyframes axis-ticker-inline {  from { transform: translateX(0); }  to { transform: translateX(-50%); }}57@media (prefers-reduced-motion: reduce) {  .axis-ticker__track {    animation: none;    flex-wrap: wrap;    width: 100%;  }}</style>

Annotated snippet

  1. Line 2The readable update text appears once outside the duplicated rail. The moving rail is visual reinforcement, not the only source of information.
    PitfallIs a scroll-linked ticker accessible?

    It can be if the information is available once as normal text and duplicated visual copies are aria-hidden. Do not make users chase moving text to understand status. Provide pause behavior for interactive rails and a static reduced-motion layout.

  2. Line 5The viewport clips the rail and is aria-hidden because its content is duplicated for the seamless loop. Assistive technology should not read the same ticker items twice.
    PitfallIs a scroll-linked ticker accessible?

    It can be if the information is available once as normal text and duplicated visual copies are aria-hidden. Do not make users chase moving text to understand status. Provide pause behavior for interactive rails and a static reduced-motion layout.

  3. Line 6The track contains two identical copies. Moving to -50% lands on the seam between copies, so the fallback marquee wraps without a visible jump.

    Translating the full -100% sweeps the rail off-screen before the loop snaps back; -50% lands exactly on the duplicate seam so the wrap is invisible.

    PitfallHow do I keep a marquee loop seamless?

    Duplicate the visual items and translate exactly one copy width, usually -50% when the two copies are inside the same track. If the gap is part of the track, include the gap in the travel distance or pad the track consistently so the seam does not jump.

  4. Line 38The timed animation is the fallback for browsers without scroll-driven animations. It uses the same transform keyframe as the scroll-linked version so the behavior stays conceptually identical.
    PitfallDo I still need a timed fallback for animation-timeline?

    Yes. Scroll-driven animations are modern but not universal. Ship a normal duration-based animation first, then override it inside @supports (animation-timeline: scroll()). Unsupported browsers still see a regular marquee instead of a frozen rail.

  5. Line 42scroll(nearest inline) binds progress to the inline axis. For a vertical rail, switch both the timeline axis and keyframe to block/translateY instead of silently reusing horizontal motion.

    Scroll each viewport horizontally. Both bind scroll(nearest inline); translateY ignores inline progress and slides chips off-axis, while translateX matches and runs the rail.

    PitfallWhat does axis-aware mean for a scroll-linked marquee?

    The scroll timeline must match the direction of the rail. A horizontal ticker should bind to scroll(nearest inline) and animate translateX. A vertical ticker should bind to scroll(nearest block) and animate translateY. Mixing those axes makes the rail respond to the wrong scroll progress.

  6. Line 57Reduced motion removes both the timed fallback and scroll-linked movement, then lets the duplicated items wrap statically inside the same viewport.

Other pitfalls

Are scroll-linked marquees expensive?
A transform-only rail is generally cheap, but long tracks with many chips, masks, shadows, or nested media can add paint and compositing cost. Keep the rail shallow, avoid layout animation, and test low-end mobile scrolling.

Notes

Overview

Marquee tickers are simple in concept (move a strip of content sideways) and full of small failure modes: how do you make the loop seamless, what happens on a vertical-axis variant, what happens to screen readers when content scrolls under them. This pattern documents axis ownership (X vs Y), duplicate-track accessibility (the dupe stays aria-hidden so AT users hear the content once), and a static prefers-reduced-motion fallback that keeps the full content visible without scroll.

When to use it

Reach for marquee tickers when you have a stream of equal-weight items that benefit from constant motion — partner logos, stock prices, news headlines, social proof bars. Skip them for paragraphs of body copy; scrolling text readers cannot pause is hostile. Skip them in any context where the content needs to be acted on (CTAs, form fields, error messages) — users cannot click a moving target comfortably.

How it works

The track contains two copies of the same content row, side by side. The track animates transform: translateX(0) to translateX(-50%) on infinite loop — at -50% the view shows the start of the second copy, which is identical to the start of the first copy, so the loop wraps without a visible seam. The vertical variant swaps translateX for translateY and orients the track as a flex column. The hover-pause ribbon variant pauses the animation on :hover + :focus-within so users can read individual items.

Production gotchas

The second copy MUST be aria-hidden="true" or screen readers announce every item twice. translateX(-50%) depends on the track width being exactly the sum of two identical content widths — if items have variable widths or gaps differ between the original and the duplicate, the seam will visibly hop. Animating left or margin-left instead of transform forces layout on every frame — always use transform for the marquee. iOS Safari throttles offscreen marquee animations aggressively in low power mode; the loop will appear paused, which is actually desirable behavior.

Accessibility

Live scrolling text is one of the worst patterns for users with cognitive or vestibular disabilities, so the hover-pause variant ships as the recommended default for interactive contexts. Under prefers-reduced-motion: reduce the animation stops and the duplicate track switches to display: none so the single non-scrolling row remains. The duplicate is always aria-hidden, regardless of motion preference.

References

Implementation depth

The hard part is ownership of axes. Let vertical scroll decide progress while the ticker track owns horizontal translation; mixing both concerns in one element makes the marquee feel like it slips under the user instead of responding to scroll.

The duplicate track is still an accessibility boundary. Hide the duplicate with aria-hidden, keep the readable source in DOM order, and freeze the track under reduced motion so scroll remains a navigation cue rather than a forced animation.