Want to create a stock ticker style animation that scrolls infinitely? In this tutorial, we'll show you how we built the trending hashtags ticker on Skyscraper Tools using pure CSS – no JavaScript required.
Live Demo
Here's what we're building:
The Key Technique: Duplicate Content
The secret to seamless infinite scrolling is duplicating the content. When you translate the track by -50%, the duplicate content takes over exactly where the original was, creating a perfect loop.
HTML Structure
The HTML is straightforward – a container, a track, and duplicated content:
<div class="ticker-container">
<div class="ticker-track">
<!-- First copy of content -->
<div class="ticker-content">
<span class="ticker-item">#Bluesky</span>
<span class="ticker-item">#Trending</span>
<span class="ticker-item">#News</span>
<span class="ticker-item">#Tech</span>
</div>
<!-- Duplicate for seamless loop -->
<div class="ticker-content">
<span class="ticker-item">#Bluesky</span>
<span class="ticker-item">#Trending</span>
<span class="ticker-item">#News</span>
<span class="ticker-item">#Tech</span>
</div>
</div>
</div>
CSS Animation
Here's the complete CSS with detailed comments:
/* Container with overflow hidden to clip the animation */
.ticker-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.3);
padding: 10px 0;
overflow: hidden;
}
/* Gradient fade on edges */
.ticker-container::before,
.ticker-container::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 60px;
z-index: 2;
pointer-events: none;
}
.ticker-container::before {
left: 0;
background: linear-gradient(to right, rgba(0,0,0,0.3), transparent);
}
.ticker-container::after {
right: 0;
background: linear-gradient(to left, rgba(0,0,0,0.3), transparent);
}
/* The animated track */
.ticker-track {
display: flex;
animation: ticker-scroll 60s linear infinite;
width: fit-content;
}
/* Content containers side by side */
.ticker-content {
display: flex;
gap: 32px;
padding: 0 16px;
white-space: nowrap;
}
/* Individual items */
.ticker-item {
color: white;
font-weight: 600;
font-size: 15px;
}
/* The animation - translate by -50% (half the track) */
@keyframes ticker-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
How It Works
1. Flexbox Layout
The .ticker-track uses flexbox to place both content copies side-by-side. With width: fit-content, the track is exactly as wide as its content.
2. The Animation
The ticker-scroll animation translates the track from 0 to -50%. Since we have two identical copies, when the first copy moves off-screen, the second copy is in exactly the same position the first started.
3. Linear Timing
Using linear timing function ensures constant speed with no acceleration or deceleration. The infinite keyword makes it loop forever.
4. Edge Gradients
The ::before and ::after pseudo-elements create gradient overlays that fade the content at the edges, giving a polished look.
Customization Options
Change Speed
Adjust the animation duration. Larger values = slower scrolling:
animation: ticker-scroll 30s linear infinite; /* Faster */
animation: ticker-scroll 120s linear infinite; /* Slower */
Pause on Hover
Add hover interaction to pause the animation:
.ticker-track:hover {
animation-play-state: paused;
}
Reverse Direction
Scroll right-to-left instead:
@keyframes ticker-scroll-reverse {
0% {
transform: translateX(-50%);
}
100% {
transform: translateX(0);
}
}
Vertical Ticker
For vertical scrolling, use flex-direction: column and translateY:
.ticker-track {
flex-direction: column;
animation: ticker-scroll-vertical 30s linear infinite;
}
@keyframes ticker-scroll-vertical {
0% { transform: translateY(0); }
100% { transform: translateY(-50%); }
}
Server-Side Rendering
In the Skyscraper Tools implementation, we render the ticker HTML on the server with dynamic data:
// Fetch trending hashtags
const trendingHashtags = await getTrendingHashtags('1h', 20);
// Build ticker HTML with duplicated content
const tickerHtml = `
<div class="ticker-container">
<div class="ticker-track">
<div class="ticker-content">
${trendingHashtags.map(h =>
`<a href="https://bsky.app/hashtag/${h.tag}"
class="ticker-item">#${h.tag}</a>`
).join('')}
</div>
<div class="ticker-content">
${trendingHashtags.map(h =>
`<a href="https://bsky.app/hashtag/${h.tag}"
class="ticker-item">#${h.tag}</a>`
).join('')}
</div>
</div>
</div>
`;
Performance Considerations
- Use transform – CSS transforms are GPU-accelerated, unlike animating
leftormargin - Avoid will-change abuse – Only add
will-change: transformif you notice jank - Limit content – Don't add hundreds of items; 10-30 is usually sufficient
- Test on mobile – Ensure smooth scrolling on lower-powered devices
Browser Support
This technique works in all modern browsers:
- Chrome 43+
- Firefox 16+
- Safari 9+
- Edge 12+
- iOS Safari 9+
- Android Chrome 43+
See It Live
Visit Skyscraper Tools to see this ticker in action with live trending Bluesky hashtags.