Live Match Scoreboard — Free Sports Scoreboard HTML CSS JS
Live Match Scoreboard · Dashboards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="lms-card">
<div class="lms-status">
<span class="lms-live-dot"></span>
<span>LIVE — 2nd Set</span>
</div>
<div class="lms-board">
<div class="lms-team">
<span class="lms-team-name">Ridgeview Hawks</span>
<span class="lms-score" id="lmsScoreA">14</span>
</div>
<div class="lms-mid">
<span class="lms-clock" id="lmsClock">18:42</span>
<span class="lms-sep">SET 2</span>
</div>
<div class="lms-team lms-team-b">
<span class="lms-score" id="lmsScoreB">11</span>
<span class="lms-team-name">Northfield Otters</span>
</div>
</div>
<div class="lms-sets" id="lmsSets"></div>
<div class="lms-controls">
<button type="button" class="lms-btn" id="lmsAddA">+1 Hawks</button>
<button type="button" class="lms-btn lms-btn-b" id="lmsAddB">+1 Otters</button>
<button type="button" class="lms-btn lms-btn-ghost" id="lmsAuto">Auto-play</button>
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0f1a;color:#eef1f7;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:32px 16px}
.lms-card{background:#111a2b;border:1px solid #22304a;border-radius:18px;padding:22px;width:100%;max-width:440px}
.lms-status{display:flex;align-items:center;gap:7px;font-size:11.5px;font-weight:800;color:#f87171;letter-spacing:.05em;text-transform:uppercase;margin-bottom:18px}
.lms-live-dot{width:7px;height:7px;border-radius:50%;background:#ef4444;animation:lmsPulse 1.4s ease-in-out infinite}
@keyframes lmsPulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.75)}}
.lms-board{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:14px;margin-bottom:16px}
.lms-team{display:flex;flex-direction:column;gap:6px}
.lms-team-b{align-items:flex-end;text-align:right}
.lms-team-name{font-size:12.5px;font-weight:700;color:#9aa5bd}
.lms-score{font-size:44px;font-weight:900;line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums;transition:transform .18s ease}
.lms-score.lms-flash{animation:lmsFlash .5s ease}
@keyframes lmsFlash{0%{color:#4ade80;transform:scale(1.18)}100%{color:#eef1f7;transform:scale(1)}}
.lms-mid{display:flex;flex-direction:column;align-items:center;gap:4px}
.lms-clock{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums;color:#eef1f7;background:#1a2438;border-radius:8px;padding:5px 10px}
.lms-sep{font-size:9.5px;font-weight:800;color:#5c6a89;letter-spacing:.08em}
.lms-sets{display:flex;justify-content:center;gap:6px;margin-bottom:18px}
.lms-set-pill{font-size:10.5px;font-weight:700;color:#5c6a89;background:#161f33;border:1px solid #22304a;border-radius:999px;padding:4px 9px}
.lms-set-pill.lms-set-a{color:#4ade80;border-color:#245139}
.lms-set-pill.lms-set-b{color:#60a5fa;border-color:#233c66}
.lms-controls{display:flex;gap:8px}
.lms-btn{flex:1;background:#1e3a8a;color:#fff;border:none;border-radius:10px;padding:11px;font-size:12.5px;font-weight:700;cursor:pointer;transition:opacity .15s}
.lms-btn:hover{opacity:.85}
.lms-btn-b{background:#0369a1}
.lms-btn-ghost{background:transparent;border:1px solid #22304a;color:#9aa5bd}
.lms-btn-ghost.lms-active{background:#1a2438;color:#eef1f7;border-color:#3b4a6b}var scoreA = 14;
var scoreB = 11;
var setsWon = ['A', 'B', 'A']; // completed sets so far, most recent last
var clockSeconds = 18 * 60 + 42;
var scoreAEl = document.getElementById('lmsScoreA');
var scoreBEl = document.getElementById('lmsScoreB');
var clockEl = document.getElementById('lmsClock');
var setsEl = document.getElementById('lmsSets');
function renderSets() {
setsEl.innerHTML = setsWon.map(function (winner, i) {
var cls = winner === 'A' ? 'lms-set-a' : 'lms-set-b';
var label = winner === 'A' ? 'Hawks' : 'Otters';
return '<span class="lms-set-pill ' + cls + '">Set ' + (i + 1) + ' \u00b7 ' + label + '</span>';
}).join('');
}
function renderClock() {
var m = Math.floor(clockSeconds / 60);
var s = clockSeconds % 60;
clockEl.textContent = m + ':' + (s < 10 ? '0' : '') + s;
}
function flash(el) {
el.classList.remove('lms-flash');
// Force a reflow so the animation restarts even if it fires again quickly.
void el.offsetWidth;
el.classList.add('lms-flash');
}
function addPoint(team) {
if (team === 'A') { scoreA++; scoreAEl.textContent = scoreA; flash(scoreAEl); }
else { scoreB++; scoreBEl.textContent = scoreB; flash(scoreBEl); }
}
document.getElementById('lmsAddA').addEventListener('click', function () { addPoint('A'); });
document.getElementById('lmsAddB').addEventListener('click', function () { addPoint('B'); });
var autoTimer = null;
var clockTimer = null;
var autoBtn = document.getElementById('lmsAuto');
function startAuto() {
autoTimer = setInterval(function () {
addPoint(Math.random() < 0.5 ? 'A' : 'B');
}, 1800);
clockTimer = setInterval(function () {
clockSeconds++;
renderClock();
}, 1000);
autoBtn.textContent = 'Stop auto-play';
autoBtn.classList.add('lms-active');
}
function stopAuto() {
clearInterval(autoTimer);
clearInterval(clockTimer);
autoBtn.textContent = 'Auto-play';
autoBtn.classList.remove('lms-active');
}
autoBtn.addEventListener('click', function () {
if (autoTimer) stopAuto(); else startAuto();
});
renderSets();
renderClock();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Live Match Scoreboard — Two Teams, a Clock, and a Flash on Every Score Change

A good live scoreboard needs to answer three questions at a glance — who's ahead, how far into the match are we, and did something just happen — without the viewer having to stare and compare numbers. This snippet builds that in plain HTML, CSS, and vanilla JavaScript: two team scores, a running match clock, a row of completed-set history pills, and a genuine flash animation the instant either score changes.
A flash that restarts even on rapid updates
The signature detail is flash(): it removes the .lms-flash class, forces a synchronous reflow with void el.offsetWidth, and *then* re-adds the class. That reflow-forcing line matters — without it, if a score updates again while the previous flash animation is still finishing, simply re-adding a class that's already present does nothing, because the browser has no new state change to animate from. Forcing a reflow in between guarantees the flash restarts cleanly every single time, even during a burst of rapid scoring.
Tabular numbers keep the layout stable
Both the score and the clock use font-variant-numeric: tabular-nums, so digits don't shift the surrounding layout as they change width (a "1" next to an "11" would otherwise nudge things around) — small, but it's the difference between a scoreboard that feels solid and one that visibly jitters on every update.
Two independent timers, cleanly started and stopped
Auto-play runs two separate setInterval timers — one incrementing a random team's score every 1.8s, one advancing the match clock every second — started together and both cleared together by stopAuto(). Keeping them as two separate intervals (rather than one interval doing both jobs) means the scoring cadence and the clock cadence can be tuned independently, which matches how a real match actually behaves: the clock runs continuously while scoring is sporadic.
Set history as a simple derived row
setsWon is just an ordered array of 'A'/'B' characters, and renderSets() maps it straight to a row of colored pills — a lightweight way to show match history (who won set 1, set 2, and so on) without a heavier stats table.
Manual and automatic controls coexist
The "+1 Hawks" / "+1 Otters" buttons and the auto-play toggle both call the same addPoint() function, so manual scoring during a live event and a demo/simulated auto-play mode share identical logic — there's no separate code path to keep in sync.
Customizing it
Replace the auto-play random scoring with a real WebSocket or polling feed calling addPoint() on genuine score events, add a period/quarter indicator instead of sets for other sports, or extend setsWon with actual set scores. Pair it with a tournament match bracket for the full event, or a live vote bar race for a different kind of live-updating comparison.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA scoreboard renders mid-match with two team scores, a clock, and set history pills.
- 2Click "+1 Hawks" or "+1 Otters"That team's score increments with a brief green flash-highlight.
- 3Click "Auto-play"Scores update randomly every 1.8s and the clock advances every second.
- 4Click "Stop auto-play"Both timers clear immediately.
- 5Score rapidlyNotice the flash animation restarts cleanly even on back-to-back updates.
- 6Connect a real feedCall addPoint('A') or addPoint('B') from your live-scoring WebSocket or API poll.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
If a score updates again while the previous flash animation is still playing, simply removing and immediately re-adding the same CSS class does nothing visually, because the browser batches the class changes and never sees the class actually leave and return. Reading el.offsetWidth between the remove and the re-add forces the browser to synchronously recalculate layout at that point, which flushes the class removal before the class is re-added — guaranteeing the animation restarts cleanly every time, even during a fast scoring streak.
A real match's clock runs continuously and predictably (once per second) while scoring happens sporadically and at a different rhythm, so using two independent setInterval calls lets each cadence be tuned without affecting the other. It also means the clock keeps ticking accurately even while, in a real integration, score updates might arrive irregularly from a WebSocket rather than on a fixed timer.
Replace the auto-play interval\'s random selection with your WebSocket message handler or polling logic, calling addPoint(\'A\') or addPoint(\'B\') whenever a real point is scored for that team — addPoint() already handles incrementing the score, updating the DOM, and triggering the flash animation, so no other changes are needed. Drive clockSeconds from your feed\'s authoritative match time in the same way, calling renderClock() after updating it.
Most fonts render digits at different widths (a "1" is narrower than a "4"), so without tabular-nums a score changing from 9 to 10, or a clock ticking from 19:59 to 20:00, would cause the surrounding layout to visibly shift as the digit count or digit widths change. Tabular-nums forces every digit to the same fixed width, keeping the scoreboard\'s layout perfectly stable through every update.
Hold scoreA, scoreB, clockSeconds, and setsWon in component state, and trigger the flash animation with a short-lived state flag toggled on and off (or by keying the score element with a changing key prop in React to force a remount) rather than manually manipulating classList — frameworks generally handle "restart an animation on repeated state changes" via key-based remounting instead of the manual reflow trick this vanilla version uses.