HTML
<div class="build-stage">
<div class="build-card">
<div class="build-header">
<div class="build-title" id="build-title">⏳ Running</div>
<div class="build-branch">feat/badge-v5 · a3f9c2e</div>
</div>
<div class="build-steps" id="build-steps"></div>
<div class="build-footer">
<span id="build-elapsed">Elapsed: 0s</span>
<button class="build-restart" id="build-restart" type="button">↺ Restart demo</button>
</div>
</div>
</div> CSS
.build-stage {
background: #0d1117;
padding: 40px 32px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 460px;
}
.build-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
width: 100%;
max-width: 420px;
overflow: hidden;
font-family: ui-monospace, "JetBrains Mono", monospace;
}
.build-header {
padding: 14px 18px;
border-bottom: 1px solid #21262d;
display: flex;
justify-content: space-between;
align-items: center;
}
.build-title {
font-size: 12px;
letter-spacing: 0.06em;
color: #e6edf3;
}
.build-branch {
font-size: 10px;
color: #8b949e;
letter-spacing: 0.06em;
}
.build-steps { padding: 8px 0; }
.build-step {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 18px;
transition: background 0.3s;
}
.build-step.is-active { background: rgba(255,255,255,0.03); }
.build-step-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.build-spinner-arc {
transform-origin: 10px 10px;
animation: build-spin 0.8s linear infinite;
}
@keyframes build-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.build-spinner-arc { animation: none; }
}
.build-step-info { flex: 1; }
.build-step-name {
font-size: 12px;
color: #e6edf3;
letter-spacing: 0.02em;
line-height: 1;
margin-bottom: 3px;
}
.build-step-name.is-dim { color: #484f58; }
.build-step-meta {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 10px;
letter-spacing: 0.1em;
color: #8b949e;
text-transform: uppercase;
}
.build-step-dur {
font-size: 11px;
color: #8b949e;
letter-spacing: 0.06em;
flex-shrink: 0;
}
.build-footer {
padding: 12px 18px;
border-top: 1px solid #21262d;
font-size: 10px;
letter-spacing: 0.08em;
color: #8b949e;
display: flex;
justify-content: space-between;
}
.build-restart {
cursor: pointer;
color: #4a90d9;
text-decoration: underline;
background: none;
border: none;
font-family: inherit;
font-size: inherit;
letter-spacing: inherit;
} JS
// Build pipeline — 6 sequential CI stages, each takes a few seconds.
// Active step shows a spinning arc; completed steps show a checkmark.
// Restarts automatically 3s after completion.
(function () {
var STEPS = [
{ label: 'Checkout', meta: 'actions/checkout@v4', dur: 2 },
{ label: 'npm install', meta: 'node 20 · 1,284 packages', dur: 14 },
{ label: 'ESLint', meta: '0 errors · 3 warnings', dur: 5 },
{ label: 'Jest', meta: '142 tests · 3 suites', dur: 24 },
{ label: 'Vite build', meta: '1.4 MB bundle', dur: 18 },
{ label: 'fly.io deploy', meta: 'prod · us-east region', dur: 9 }
];
var active = 0;
var elapsed = 0;
var elapsedTimer;
var stepsEl = document.getElementById('build-steps');
var titleEl = document.getElementById('build-title');
var elapsedEl = document.getElementById('build-elapsed');
var restartBtn = document.getElementById('build-restart');
function iconDone() {
return '<svg class="build-step-icon" viewBox="0 0 20 20">'
+ '<circle cx="10" cy="10" r="9" fill="rgba(63,185,80,0.12)" stroke="rgba(63,185,80,0.4)" stroke-width="1.5"/>'
+ '<path d="M 5.5 10.5 L 8.5 13.5 L 14.5 7" fill="none" stroke="#3fb950" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
+ '</svg>';
}
function iconRunning() {
return '<svg class="build-step-icon" viewBox="0 0 20 20">'
+ '<circle cx="10" cy="10" r="8" fill="none" stroke="#30363d" stroke-width="2"/>'
+ '<circle cx="10" cy="10" r="8" fill="none" stroke="#e3b341" stroke-width="2" stroke-dasharray="18 32" stroke-linecap="round" class="build-spinner-arc"/>'
+ '</svg>';
}
function iconWaiting() {
return '<svg class="build-step-icon" viewBox="0 0 20 20">'
+ '<circle cx="10" cy="10" r="9" fill="none" stroke="#30363d" stroke-width="1.5"/>'
+ '</svg>';
}
function render() {
stepsEl.innerHTML = STEPS.map(function (s, i) {
var done = i < active;
var running = i === active;
var waiting = i > active;
var icon = done ? iconDone() : running ? iconRunning() : iconWaiting();
var nameClass = waiting ? 'build-step-name is-dim' : 'build-step-name';
var dur = done ? s.dur + 's' : running ? elapsed + 's' : '';
var durColor = done ? '#3fb950' : running ? '#e3b341' : '#30363d';
return '<div class="build-step ' + (running ? 'is-active' : '') + '">'
+ icon
+ '<div class="build-step-info">'
+ '<div class="' + nameClass + '">' + s.label + '</div>'
+ '<div class="build-step-meta" style="color:' + (waiting ? '#30363d' : '') + '">'
+ (waiting ? '—' : s.meta) + '</div>'
+ '</div>'
+ '<div class="build-step-dur" style="color:' + durColor + '">' + dur + '</div>'
+ '</div>';
}).join('');
elapsedEl.textContent = 'Elapsed: ' + STEPS.slice(0, active).reduce(function (a, s) { return a + s.dur; }, 0) + 's';
if (active >= STEPS.length) {
titleEl.textContent = '✓ Passed';
titleEl.style.color = '#3fb950';
} else {
titleEl.textContent = '⏳ Running';
titleEl.style.color = '#e3b341';
}
}
function start() {
active = 0;
elapsed = 0;
clearInterval(elapsedTimer);
render();
elapsedTimer = setInterval(function () { elapsed++; render(); }, 1000);
var advance = setInterval(function () {
elapsed = 0;
active++;
render();
if (active >= STEPS.length) {
clearInterval(advance);
clearInterval(elapsedTimer);
setTimeout(start, 3000);
}
}, 2500);
}
restartBtn.addEventListener('click', start);
start();
})();