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:

#Bluesky #Trending #News #Tech #Sports #Politics #Gaming #Music
#Bluesky #Trending #News #Tech #Sports #Politics #Gaming #Music

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:

HTML
<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:

CSS
/* 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:

TypeScript (Express/Node.js)
// 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 left or margin
  • Avoid will-change abuse – Only add will-change: transform if 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.

Related Tutorials

Download Skyscraper for iOS →