The Ticker component creates an animated horizontal scrolling display of word tags. It supports multiple rows, shuffle randomization, different visual variants, and smooth animations with hover interactions.
Import
import Ticker from "../components/Ticker.astro";
Usage
Basic Usage
src/pages/[lang]/index.astro
---
import Ticker from "../../components/Ticker.astro";
import { getCollection } from "astro:content";
const words = await getCollection("words-es");
const wordList = words.map((w) => w.data.word);
const shuffled = wordList.sort(() => Math.random() - 0.5).slice(0, 30);
---
<Ticker words={shuffled} lang="es" />
Vibrant Variant
<Ticker
words={featuredWords}
lang="en"
variant="vibrant"
rows={1}
/>
Multiple Rows
<Ticker
words={allWords}
lang="es"
variant="default"
rows={3}
/>
Props
Array of words to display in the ticker. Words are automatically shuffled and distributed across rows.Example: ["Chapín", "Catracho", "Guanaco", "Tico"]
The current language code. Used for generating correct URLs to word detail pages.Values: "es" or "en"
variant
'default' | 'vibrant'
default:"default"
Visual style variant:
"default" - Clean, minimal styling with subtle animations
"vibrant" - Enhanced gradients, glows, and animated effects
Number of horizontal rows to display. Words are automatically distributed evenly across rows.
Features
Word Shuffling
Words are shuffled on both server and client side:
function shuffle<T>(arr: T[]): T[] {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
const shuffled = shuffle(words);
Row Distribution
Words are evenly distributed across the specified number of rows:
const perRow = Math.ceil(shuffled.length / numRows);
const baseRows = Array.from({ length: numRows }, (_, i) =>
shuffled.slice(i * perRow, (i + 1) * perRow)
);
Alternating Animation Directions
Rows alternate between left-to-right and right-to-left scrolling:
<div
class={`ticker-content ${
idx % 2 === 1
? "animate-[scroll_28s_linear_infinite_reverse]"
: "animate-[scroll_22s_linear_infinite]"
}`}
>
Pause on Hover
Animations pause when hovering over a row:
.ticker-row:hover .ticker-content {
animation-play-state: paused;
}
Edge Fade Gradients
Smooth fade effects on the left and right edges:
<div class="ticker-fade-left" />
<div class="ticker-fade-right" />
.ticker-fade-left {
left: 0;
background: linear-gradient(to right, var(--background) 0%, transparent 100%);
}
.ticker-fade-right {
right: 0;
background: linear-gradient(to left, var(--background) 0%, transparent 100%);
}
Variants
Default Variant
Clean, minimal styling with subtle hover effects:
- Simple border and background
- Minimal shadows
- Clean text rendering
- Subtle hover transforms
Vibrant Variant
Enhanced visual effects with gradients and glows:
.ticker--vibrant .ticker-row {
border: 1px solid rgba(96, 165, 250, 0.18);
background:
radial-gradient(circle at 30% 20%, rgba(96, 165, 250, 0.2), transparent 60%),
linear-gradient(135deg, rgba(30, 41, 59, 0.85), rgba(15, 23, 42, 0.88));
border-radius: 20px;
box-shadow:
0 8px 28px -10px rgba(96, 165, 250, 0.25),
0 2px 6px rgba(0, 0, 0, 0.5);
}
Vibrant Features
- Gradient backgrounds with radial overlays
- Animated sheen effect rotating across the row
- Glowing dot indicators with pulse animation
- Text with gradient fills and hue-shift animation
- Enhanced hover effects with stronger transforms
Each word is rendered as a clickable tag:
<a
class="ticker-word"
href={`${prefix}/palabras/${slugify(w)}/`}
tabindex="-1"
aria-hidden="true"
>
<span class="ticker-dot h-2 w-2 animate-pulse rounded-xl" />
<span class="ticker-text">{w}</span>
</a>
Tag Features
- Animated pulsing dot indicator
- Hover scale and translation effects
- Theme-aware styling
- Automatic slug generation for URLs
Client-Side Rehydration
The component includes a script that runs on page load to:
- Reshuffle words for variation
- Redistribute across rows
- Clone elements for infinite scroll effect
- Set dynamic animation durations
rows.forEach((r, rowIdx) => {
// ... shuffle and populate ...
// Clone for infinite scroll
const original = [...r.children];
original.forEach((n) => r.appendChild(n.cloneNode(true)));
original.forEach((n) => r.appendChild(n.cloneNode(true)));
// Set dynamic speed
const baseDuration = 22;
const variance = 6;
const speed = baseDuration + rowIdx * variance;
r.style.animationDuration = speed + "s";
});
Animations
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-33.333%);
}
}
Pulse Glow (Vibrant)
@keyframes pulseGlow {
0%, 100% {
transform: scale(1);
box-shadow:
0 0 10px 2px rgba(96, 165, 250, 0.75),
0 0 24px -4px rgba(14, 165, 233, 0.4);
}
50% {
transform: scale(1.25);
box-shadow:
0 0 16px 4px rgba(96, 165, 250, 0.95),
0 0 36px -4px rgba(14, 165, 233, 0.6);
}
}
Hue Shift (Vibrant)
@keyframes hueShift {
0% {
filter: hue-rotate(0deg);
}
100% {
filter: hue-rotate(360deg);
}
}
Accessibility
Reduced Motion
Animations are disabled for users who prefer reduced motion:
@media (prefers-reduced-motion: reduce) {
.ticker-content {
animation: none !important;
}
.ticker-dot {
animation: none !important;
}
}
ARIA Attributes
Ticker content is marked as decorative:
<div class="ticker-content" aria-hidden="true">
<a tabindex="-1" aria-hidden="true">...</a>
</div>
Responsive Design
Mobile Adjustments
@media (max-width: 768px) {
.ticker-container {
gap: 0.625rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.ticker-word {
padding: 0.5rem 0.875rem;
font-size: 0.8125rem;
}
}
@media (max-width: 480px) {
.ticker-word {
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
}
.ticker-dot {
width: 0.375rem;
height: 0.375rem;
}
}
Theme Support
Both variants support light and dark themes:
html[data-theme="light"] .ticker--vibrant .ticker-row {
background:
radial-gradient(circle at 30% 20%, rgba(96, 165, 250, 0.4), rgba(248, 250, 252, 0.92) 60%),
linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(241, 245, 249, 0.9));
border: 1px solid rgba(73, 151, 208, 0.35);
}
- Uses
will-change: transform for smooth animations
- GPU-accelerated transforms
- Efficient cloning strategy for infinite scroll
- Conditional animation based on user preferences
Layout Examples
Hero Section
<section class="mx-auto max-w-[1100px] px-6">
<h1>Discover Guatemalan Slang</h1>
<Ticker words={featured} lang="es" variant="vibrant" rows={1} />
</section>
Multiple Rows
<section class="mx-auto max-w-[1100px] px-6">
<Ticker words={allWords} lang="en" variant="default" rows={3} />
</section>
slugify() - Convert words to URL-safe slugs