A scrolling score ticker across the top of a site is one of the most recognizable sports-media UI patterns — and one of the more approachable things to build with a football API. This guide walks through building one from scratch with vanilla HTML, CSS, and JavaScript.
What We're Building
A horizontal strip showing today's matches with live scores, auto-updating every 30 seconds, that scrolls continuously like a news ticker.
Step 1: The HTML Structure
<div class="ticker-wrapper">
<div class="ticker-track" id="tickerTrack">
<!-- match items injected here -->
</div>
</div>
Step 2: The CSS Scroll Animation
.ticker-wrapper {
overflow: hidden;
white-space: nowrap;
background: #0f172a;
padding: 10px 0;
}
.ticker-track {
display: inline-flex;
animation: scroll-left 40s linear infinite;
}
.ticker-item {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0 24px;
color: white;
font-size: 14px;
border-right: 1px solid #334155;
}
.ticker-item .live {
color: #f97316;
font-weight: bold;
}
@keyframes scroll-left {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
Step 3: Fetching and Rendering Matches
async function fetchTodayMatches() {
const today = new Date().toISOString().split('T')[0];
const res = await fetch(
`https://live-football-api.com/api/v1/matches?api_key=YOUR_KEY&date=${today}&lang=en`
);
const result = await res.json();
return result.data.matches;
}
function renderTicker(matches) {
const track = document.getElementById('tickerTrack');
// Duplicate the list so the scroll loop has no visible gap
const html = [...matches, ...matches].map(match => `
<div class="ticker-item">
<span>${match.home.name}</span>
<strong>${match.home.score} - ${match.away.score}</strong>
<span>${match.away.name}</span>
${match.status.is_live
? `<span class="live">${match.status.display}</span>`
: `<span>${match.status.display || match.kickoff}</span>`}
</div>
`).join('');
track.innerHTML = html;
}
Step 4: Keeping Scores Live
Poll on an interval to refresh live scores without a full page reload. 30 seconds is a reasonable default that balances freshness against credit usage:
async function updateTicker() {
const matches = await fetchTodayMatches();
renderTicker(matches);
}
updateTicker();
setInterval(updateTicker, 30000); // refresh every 30 seconds
Step 5: Avoiding a Visual Jump on Refresh
Re-rendering the whole ticker on each poll can cause the scroll animation to jump. A simple fix is updating only the score text inside existing DOM nodes rather than replacing the entire track:
function updateScoresInPlace(matches) {
matches.forEach(match => {
const el = document.querySelector(`[data-match-id="${match.id}"] strong`);
if (el) el.textContent = `${match.home.score} - ${match.away.score}`;
});
}
This requires adding a data-match-id attribute to each ticker item when first rendered, so subsequent updates can target the right element without rebuilding the whole strip.
Step 6: Reducing Credit Usage
Polling every 30 seconds across a full day adds up. A few ways to cut usage without hurting the experience:
- Pause polling when the browser tab isn't visible, using the Page Visibility API
- Slow the interval when there are no live matches (e.g. check once every 5 minutes instead of 30 seconds if nothing is
is_live) - Share one poll across all visitors by caching the response on your own server for a few seconds, rather than every browser tab hitting the API directly
let pollInterval;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
clearInterval(pollInterval);
} else {
updateTicker();
pollInterval = setInterval(updateTicker, 30000);
}
});
Frequently Asked Questions
How many credits does a ticker use per day?
At a 30-second refresh over 24 hours, that's roughly 2,880 calls/day per active viewer session if polling client-side — server-side caching (one shared poll for all visitors) reduces this dramatically.
Can I filter the ticker to specific leagues only?
Yes, filter the matches array client-side by match.league.id after fetching, since /matches returns all matches for the date by default.
Should I poll or use webhooks for a ticker?
Polling is simpler for a ticker showing many simultaneous matches, since webhooks deliver one event at a time per match rather than a full day's board — polling /matches is the more natural fit here.
How do I handle a day with no matches?
Check if the matches array is empty and render a fallback message ("No matches today") instead of an empty scrolling strip.