scrolling news ticker

a strip of text that endlessly scrolls sideways, like a news banner or stock ticker, no javascript required.

beginner css
you've seen this on news sites and even on my blog's homepage: a strip of text that scrolls sideways forever, looping seamlessly with no visible seam or jump. it looks like it needs javascript to keep track of the scroll position, but the whole thing can actually be done with plain css. this tutorial builds one from scratch, including the trick that makes the loop seamless instead of snapping back to the start.

what we're building

a horizontal banner containing a line of text that continuously scrolls from right to left, and loops forever without ever showing a gap or a jump back to the beginning:

live example:

welcome to my site ♡ thanks for stopping by! don't forget to sign the guestbook welcome to my site ♡ thanks for stopping by! don't forget to sign the guestbook

step 1 — the html: duplicate the content

this is the part that trips people up the most, so it comes first. the html isn't just one copy of your text, it's two identical copies sitting right next to each other:

<div class="ticker">
  <div class="ticker-track">
    <span>welcome to my site ♡</span>
    <span>thanks for stopping by!</span>
    <span>don't forget to sign the guestbook</span>

    <span>welcome to my site ♡</span>
    <span>thanks for stopping by!</span>
    <span>don't forget to sign the guestbook</span>
  </div>
</div>

the outer .ticker is the little window the text scrolls through, it stays a fixed size and hides anything outside it. the inner .ticker-track is the strip that actually moves, and it's twice as long as it needs to be, because it contains your message twice in a row.

why duplicate it? think of the track as one long strip of text glued end to end with a second copy of itself. as the first copy slides out of view on the left, the second copy is sliding into view right behind it, seamlessly picking up exactly where the first one left off. if there were only one copy, you'd see empty blank space the moment it finished scrolling past, right before it awkwardly jumped back to the start. the duplicate is what hides that seam.

step 2 — the outer window

the outer wrapper's job is simple: be a fixed-size box, and hide anything that spills outside of it.

.ticker {
  width: 100%;
  overflow: hidden;
  border: 1px solid #e7d0dd;
  border-radius: 4px;
  background: #fbf3f8;
  padding: 10px 0;
}
  • overflow: hidden: this is the single most important line here. the inner track is going to be much wider than this box (remember, it's the text doubled up), and without this, the whole doubled strip would just spill out sideways and be fully visible instead of scrolling through a small window
  • width: 100%: makes the ticker stretch to fill whatever container it's placed in. you could give it a fixed pixel width instead if you want a smaller, contained ticker rather than a full-width banner
  • the border, background, and padding are purely decorative, style this however fits your theme, none of it affects how the scrolling works

step 3 — the moving track

the inner track is what actually scrolls. its width needs to come from its content, not be forced to match its parent, otherwise the browser has nothing to measure the scroll distance against:

.ticker-track {
  display: flex;
  width: max-content;
  white-space: nowrap;
  animation: tickerMove 14s linear infinite;
  will-change: transform;
}
  • display: flex: lays all the <span>s out in a single horizontal row, side by side, instead of stacking them or letting them wrap
  • width: max-content: tells the track to size itself to exactly as wide as its content needs, rather than shrinking to fit the parent's width. this is essential, if the track were stuck at 100% width like its parent, the text inside would just get squeezed or wrapped instead of flowing out sideways
  • white-space: nowrap: a backup measure that stops the browser from ever wrapping the text onto a second line, keeping everything on one continuous horizontal strip
  • animation: tickerMove 14s linear infinite: this is what actually moves it, and it's explained fully in the next step
  • will-change: transform: a hint to the browser that this element's transform is about to change repeatedly, so it should set up an optimized rendering layer for it ahead of time instead of recalculating one on the fly. without this, some browsers can produce a tiny stutter or hitch right at the exact moment the animation loops back to from, since that's when the browser is doing the most work resetting position. this single line smooths that out
tip: will-change isn't something to sprinkle on every element "just in case". it asks the browser to reserve extra memory and gpu resources for that element the whole time the page is open, so overusing it can actually hurt performance instead of helping. it's meant for elements you already know are about to animate constantly, like this ticker track, not as a general-purpose speed boost.

step 4 — the animation itself

the movement comes from a @keyframes rule, which is just a named set of instructions describing how something should look at different points in time:

@keyframes tickerMove {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}
  • from and to: the starting and ending state of the animation. the browser automatically fills in every frame in between, smoothly moving from one to the other, you never have to calculate the in-between positions yourself
  • translateX(0): at the very start, the track sits at its normal position, not moved at all
  • translateX(-50%): by the end, the track has slid left by 50% of its own width. since the track is exactly double-length (remember step 1), sliding it left by half its width moves it by exactly one full copy of your original text. the second copy is now sitting precisely where the first copy started, so the loop can snap back to from and repeat with nobody noticing
this is the key relationship to remember: the duplicate-content trick from step 1 and the translateX(-50%) here are two halves of the same idea. the animation only ends up looking seamless because the content is duplicated exactly once (making the track exactly 200% as wide as one copy), and the animation moves it by exactly 50% of that doubled width (which equals 100% of one copy). if you tripled the content instead, you'd need to move it by -33.333% instead, to land on exactly one copy's worth of movement.

back on the .ticker-track rule, the animation shorthand ties the keyframes to actual movement:

  • tickerMove: the name of the @keyframes rule to run, has to match exactly
  • 14s: how long one full loop takes. bigger number, slower scroll. this is the main dial to adjust reading speed, more text usually needs a longer duration so it doesn't fly by unreadably fast
  • linear: the timing curve. unlike the ease curve used in hover animations, a ticker should move at a perfectly constant speed the whole time, linear means no speeding up or slowing down, which is what makes a continuous scroll feel smooth instead of jerky
  • infinite: repeats the animation forever instead of stopping after one loop. this is what makes it a proper ticker instead of a one-time scroll

step 5 (bonus) — pause on hover

a nice touch for readability: let people pause the scroll by hovering over it, so they can actually read a line that catches their eye.

.ticker:hover .ticker-track {
  animation-play-state: paused;
}
  • animation-play-state: a property that can be set to either running (the default) or paused. setting it to paused freezes the animation exactly where it currently is, instead of resetting it, and it resumes from that same spot once the rule stops applying
  • .ticker:hover .ticker-track: notice the hover is listening on the outer .ticker wrapper, but the property changes on the inner .ticker-track. this means hovering anywhere inside the ticker's window pauses the scroll, not just hovering the text itself

live example: hover over this one to pause it.

hover to pause me! see, i stopped moving move your mouse away and i'll continue hover to pause me! see, i stopped moving move your mouse away and i'll continue
accessibility note: continuous motion like this can be uncomfortable or distracting for some visitors, the same as the hover effects covered in the previous tutorial. respect the prefers-reduced-motion setting so the ticker holds still for anyone who's turned that on, instead of scrolling regardless:
@media (prefers-reduced-motion: reduce) {
  .ticker-track {
    animation: none;
  }
}

result

a fully css-driven scrolling banner: no javascript, no timers, no manually tracking scroll position. the whole illusion rests on two things working together, doubling the content once, and moving it by exactly half its own width. that same relationship (duplicate content, then translate by the matching fraction) is the exact same trick behind most infinite marquees, logo carousels, and auto-scrolling image strips you'll see around the web, so this pattern is worth remembering beyond just text banners.