Create a sports-style digital display with this HTML scoreboard template using HTML5, CSS3, and JavaScript. Inspired by a physical scoreboard, the design features a dark panel, bold white dividers, large red LED-style scores, and a compact match timer.
The responsive scoreboard includes team-name inputs above the display and score controls below it. You can add points, deduct a point, reset both scores, and control the timer through clearly labeled buttons. Its compact layout is designed for one-screen viewing on typical desktop and mobile devices.
What you will build
You will build an interactive scoreboard with these features:
A dark scoreboard panel with rounded corners and white borders.
Two side-by-side team columns.
Editable team names positioned above the board.
Large red LED-style scores with leading zeros.
Separate +1, +2, and −1 controls for each team.
A Reset Scores button that preserves team names.
A digital match timer in minutes and seconds.
Start, Pause, and Reset Timer controls.
Score limits that prevent negative values or numbers above 99.
A responsive layout with accessible labels and keyboard controls.
HTML source
<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="A classic LED sports scoreboard with custom team names, simple score controls, and a match stopwatch.">
<title>Courtside — Digital Scoreboard</title>
</head>
<body>
<main class="app">
<header class="page-header"><span class="brand-mark" aria-hidden="true">▦</span><h1>COURTSIDE <span>THE DIGITAL SCOREBOARD</span></h1></header>
<section class="name-controls" aria-label="Team names">
<div><label for="home-name">Home Team Name</label><input id="home-name" maxlength="18" placeholder="HOME" value="HOME" autocomplete="off" spellcheck="false"></div>
<div><label for="guest-name">Guest Team Name</label><input id="guest-name" maxlength="18" placeholder="GUEST" value="GUEST" autocomplete="off" spellcheck="false"></div>
</section>
<section class="scoreboard" aria-label="Match scoreboard">
<div class="teams"><h2 id="home-heading">HOME</h2><h2 id="guest-heading">GUEST</h2></div>
<div class="scores">
<div class="score" id="home-score" role="img" aria-label="HOME score: 00"></div>
<span class="score-colon" aria-hidden="true"><i></i><i></i></span>
<div class="score" id="guest-score" role="img" aria-label="GUEST score: 00"></div>
</div>
<div class="timer-strip"><span class="timer-label">MATCH TIME</span><div id="timer" role="img" aria-label="Elapsed time: 00:00"></div></div>
</section>
<section class="score-controls" aria-label="Score controls">
<div class="team-buttons" id="home-controls" role="group" aria-label="Home score controls"><button data-team="home" data-points="1">+1</button><button data-team="home" data-points="2">+2</button><button class="subtract" data-team="home" data-points="-1">−1</button></div>
<div class="team-buttons" id="guest-controls" role="group" aria-label="Guest score controls"><button data-team="guest" data-points="1">+1</button><button data-team="guest" data-points="2">+2</button><button class="subtract" data-team="guest" data-points="-1">−1</button></div>
<button id="reset-scores" class="reset-scores">↺ <span>Reset Scores</span></button>
</section>
<section class="timer-controls" aria-label="Timer controls"><span class="control-label">STOPWATCH</span><div><button id="start" class="start">Start</button><button id="pause" disabled="">Pause</button><button id="reset-timer">Reset Timer</button></div></section>
<p class="help">Make it your match. Name your teams, add the points, and start the clock.</p>
<p id="score-status" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></p>
</main>
</body></html>
CSS source
:root {
--page: #eeefeb;
--board: #202122;
--white: #f6f5ee;
--led: #ff423d;
--led-dim: #472626;
--border: 5px;
--space: clamp(12px, 2.4vh, 24px);
font-family: Arial, Helvetica, sans-serif;
color: #272925;
background: var(--page);
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 280px; }
button, input { font: inherit; }
button { cursor: pointer; touch-action: manipulation; }
.app { width: min(100% - 36px, 720px); min-height: 100svh; margin: auto; padding: clamp(20px, 4vh, 42px) 0 20px; display: flex; flex-direction: column; justify-content: center; gap: var(--space); }
.page-header { display: flex; align-items: center; gap: 9px; margin-bottom: 4px; }
.brand-mark { font-size: 30px; line-height: 1; }
h1 { font-size: 15px; letter-spacing: 2px; margin: 0; }
h1 span { display: block; font-size: 8px; letter-spacing: 1.6px; font-weight: 500; margin-top: 5px; color: #686c64; }
.name-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
label { display: block; font-size: 11px; font-weight: 700; margin-bottom: 7px; }
input { width: 100%; min-width: 0; border: 1px solid #cbd0c5; border-radius: 5px; background: #f8f9f5; padding: 11px 13px; min-height: 44px; font-size: 13px; }
.scoreboard { background: var(--board); color: var(--white); border: var(--border) solid var(--white); border-radius: 15px; box-shadow: 0 0 0 1px #c6c8bf, 0 12px 25px #20251b1c; overflow: hidden; }
.teams { display: grid; grid-template-columns: 1fr 1fr; border-bottom: var(--border) solid var(--white); }
h2 { margin: 0; min-width: 0; padding: clamp(14px, 2.6vw, 23px) 8px; text-align: center; font-size: clamp(17px, 4.5vw, 34px); font-weight: 800; letter-spacing: .08em; overflow-wrap: anywhere; align-content: center; }
h2 + h2 { border-left: var(--border) solid var(--white); }
.scores { position: relative; display: grid; grid-template-columns: 1fr 1fr; padding: clamp(21px, 4vw, 35px) 0; }
.score { display: flex; justify-content: center; gap: 5%; padding: 0 10%; min-width: 0; }
.digit { display: block; width: 45%; height: auto; overflow: visible; }
.led-dot { fill: var(--led-dim); }
.led-dot.on { fill: var(--led); filter: drop-shadow(0 0 1.2px #ff423d66); }
.score-colon { position: absolute; top: 0; bottom: 0; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; justify-content: center; gap: clamp(15px, 4vw, 33px); }
.score-colon i { width: clamp(5px, 1.1vw, 9px); aspect-ratio: 1; border-radius: 50%; background: var(--white); }
.timer-strip { border-top: var(--border) solid var(--white); padding: 12px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.timer-label { font-size: 8px; font-weight: 700; letter-spacing: 2px; color: #bbbdb8; }
#timer { width: clamp(120px, 27vw, 180px); display: flex; gap: 7px; align-items: center; }
#timer .digit { width: 21%; }
.timer-colon { flex: 0 0 4px; height: 28px; display: flex; flex-direction: column; justify-content: space-around; }
.timer-colon i { background: var(--led); width: 4px; height: 4px; border-radius: 50%; }
.score-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px; }
.team-buttons { display: flex; justify-content: center; gap: 9px; }
button { border: 1px solid #cbd0c5; border-radius: 5px; background: #f9faf6; color: #292d26; min-height: 44px; padding: 10px 17px; font-size: 13px; font-weight: 700; transition: background .15s, transform .15s; }
.team-buttons button { flex: 1; max-width: 100px; background: #30362c; border-color: #30362c; color: white; font-size: 18px; }
.team-buttons .subtract { background: transparent; color: #30362c; border-color: #cbd0c5; }
button:hover { background: #e0e5da; }
.team-buttons button:hover { background: #4e5945; }
.team-buttons .subtract:hover { background: #e0e5da; }
button:active { transform: translateY(2px); }
button:disabled { opacity: .4; cursor: default; transform: none; }
:focus-visible { outline: 3px solid #668343; outline-offset: 4px; }
.reset-scores { grid-column: 1 / -1; justify-self: center; background: transparent; border: 0; font-size: 12px; padding: 4px 16px; }
.reset-scores span { margin-left: 5px; }
.timer-controls { border-top: 1px solid #cdd1c6; padding-top: var(--space); display: flex; align-items: center; justify-content: center; gap: 20px; }
.control-label { font-size: 9px; font-weight: 700; letter-spacing: 1.5px; color: #6c7266; }
.timer-controls > div { display: flex; gap: 7px; }
.timer-controls button { font-size: 11px; padding-inline: 15px; }
.timer-controls .start { background: #dce5cd; border-color: #bdcba9; }
.help { text-align: center; margin: 0; font-size: 11px; line-height: 1.6; color: #74786e; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
@media (max-width: 420px) {
:root { --border: 4px; --space: 14px; }
.app { width: calc(100% - 28px); padding-top: 18px; }
.name-controls { grid-template-columns: 1fr; gap: 10px; }
.name-controls > div { display: grid; grid-template-columns: 108px 1fr; align-items: center; gap: 8px; }
label { margin: 0; font-size: 10px; }
.score-controls { column-gap: 18px; row-gap: 5px; }
.team-buttons { gap: 5px; }
.team-buttons button { padding: 8px 4px; font-size: 16px; }
.timer-controls { flex-direction: column; gap: 8px; }
.help { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) { button { transition: none; } }
JavaScript source
'use strict';
// Original 5 × 7 LED glyphs; inactive dots remain visible on the board.
const glyphs = [
['01110','11011','11011','11011','11011','11011','01110'],
['00100','01100','00100','00100','00100','00100','01110'],
['01110','11011','00011','00110','01100','11000','11111'],
['11110','00011','00011','01110','00011','00011','11110'],
['00011','00111','01111','11011','11111','00011','00011'],
['11111','11000','11000','11110','00011','00011','11110'],
['01110','11000','11000','11110','11011','11011','01110'],
['11111','00011','00110','00110','01100','01100','01100'],
['01110','11011','11011','01110','11011','11011','01110'],
['01110','11011','11011','01111','00011','00011','01110']
];
const teams = { home: { name: 'HOME', score: 0 }, guest: { name: 'GUEST', score: 0 } };
const pad = number => String(number).padStart(2, '0');
const status = document.getElementById('score-status');
function digit(value) {
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('viewBox', '0 0 50 70');
svg.setAttribute('aria-hidden', 'true');
svg.classList.add('digit');
glyphs[Number(value)].forEach((row, y) => {
[...row].forEach((on, x) => {
const dot = document.createElementNS(ns, 'circle');
dot.setAttribute('cx', x * 10 + 5);
dot.setAttribute('cy', y * 10 + 5);
dot.setAttribute('r', '3.5');
dot.setAttribute('class', on === '1' ? 'led-dot on' : 'led-dot');
svg.append(dot);
});
});
return svg;
}
function renderScore(team) {
const state = teams[team];
const display = document.getElementById(`${team}-score`);
display.replaceChildren(...[...pad(state.score)].map(digit));
display.setAttribute('aria-label', `${state.name} score: ${pad(state.score)}`);
}
function updateLabels(team) {
const name = teams[team].name;
document.getElementById(`${team}-heading`).textContent = name;
document.getElementById(`${team}-controls`).setAttribute('aria-label', `${name} score controls`);
document.querySelectorAll(`[data-team="${team}"]`).forEach(button => {
const points = Number(button.dataset.points);
button.setAttribute('aria-label', `${name}: ${points > 0 ? 'add' : 'subtract'} ${Math.abs(points)} ${Math.abs(points) === 1 ? 'point' : 'points'}`);
});
renderScore(team);
}
Object.keys(teams).forEach(team => {
document.getElementById(`${team}-name`).addEventListener('input', event => {
teams[team].name = event.target.value.slice(0, 18).trim() || team.toUpperCase();
updateLabels(team);
});
updateLabels(team);
});
document.querySelectorAll('[data-points]').forEach(button => {
button.addEventListener('click', () => {
const team = button.dataset.team;
const previous = teams[team].score;
teams[team].score = Math.max(0, Math.min(99, previous + Number(button.dataset.points)));
renderScore(team);
status.textContent = `${teams[team].name}: ${pad(teams[team].score)}.${previous === teams[team].score ? ' Score limit reached.' : ''}`;
});
});
document.getElementById('reset-scores').addEventListener('click', () => {
Object.keys(teams).forEach(team => { teams[team].score = 0; renderScore(team); });
status.textContent = 'Both scores reset to 00.';
});
// Accumulate elapsed milliseconds from a monotonic clock, never interval ticks.
const maxTime = (99 * 60 + 59) * 1000;
let elapsed = 0;
let startedAt = null;
let interval = null;
let lastSecond = -1;
const timer = document.getElementById('timer');
const startButton = document.getElementById('start');
const pauseButton = document.getElementById('pause');
function currentElapsed() {
return Math.min(maxTime, elapsed + (startedAt === null ? 0 : performance.now() - startedAt));
}
function updateTimer() {
const time = currentElapsed();
const seconds = Math.floor(time / 1000);
if (seconds !== lastSecond) {
const text = `${pad(Math.floor(seconds / 60))}:${pad(seconds % 60)}`;
timer.replaceChildren(...[...text].map(character => {
if (character !== ':') return digit(character);
const colon = document.createElement('span');
colon.className = 'timer-colon';
colon.setAttribute('aria-hidden', 'true');
colon.append(document.createElement('i'), document.createElement('i'));
return colon;
}));
timer.setAttribute('aria-label', `Elapsed time: ${text}`);
lastSecond = seconds;
}
if (time >= maxTime) {
elapsed = maxTime;
stopInterval();
}
}
function stopInterval() {
clearInterval(interval);
interval = null;
startedAt = null;
startButton.disabled = elapsed >= maxTime;
pauseButton.disabled = true;
}
startButton.addEventListener('click', () => {
if (interval !== null || elapsed >= maxTime) return;
startedAt = performance.now();
interval = setInterval(updateTimer, 100);
startButton.disabled = true;
pauseButton.disabled = false;
updateTimer();
});
pauseButton.addEventListener('click', () => {
elapsed = currentElapsed();
stopInterval();
updateTimer();
});
document.getElementById('reset-timer').addEventListener('click', () => {
elapsed = 0;
stopInterval();
updateTimer();
});
updateTimer();
How it works
Enter each team’s name in the fields above the scoreboard. The headings update immediately, making it easy to prepare the display for a new match.
Use the buttons beneath each team to add one point, add two points, or subtract one point. Both scores begin at 00 and remain between 00 and 99. Select Reset Scores to return both teams to zero without changing their names or interrupting the timer.
Press Start to begin timing the match. Pause stops the timer temporarily, and Start resumes it from the same elapsed time. Reset Timer stops the clock and returns it to 00:00.
The project uses index.html for the interface, style.css for the scoreboard and LED appearance, and script.js for team names, score updates, and timer behavior. It runs in the browser without a backend. Refreshing the page clears the current match and restores the default values.
Try a change
Choose Edit code, make a small change, then press Run to preview it.
Use the source Copy buttons, choose Edit code to open the editor, or Download ZIP to save the project after a six-second countdown.





.webp)

Discussion (0)
Leave a comment