Build Pipeline
CI/CD stage tracker with a spinning arc on the active step. Sequential steps light up — you know roughly how long remains by which step is spinning. Restarts automatically.
Build Pipeline the 14th of 30 designs in the 30 CSS Badges collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
The code
<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> .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;
} // 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();
})();