12 CSS Steppers09 / 12

CSS + JSMIT licensed

CSS Circular Step Progress Indicator

A centred SVG arc ring whose stroke-dashoffset advances per step, surrounded by a tile grid of clickable step cards — violet/purple accent on a near-black background ideal for dashboard headers.

Published

Live Demo
Try it

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

The code

<div class="stp-09">
  <div class="stp-09__shell">

    <!-- Big circular ring -->
    <div class="stp-09__circle-wrap">
      <div class="stp-09__ring">
        <svg class="stp-09__svg" viewBox="0 0 200 200">
          <defs>
            <linearGradient id="stp-09-grad" x1="0%" y1="0%" x2="100%" y2="100%">
              <stop offset="0%" stop-color="#8b5cf6"/>
              <stop offset="100%" stop-color="#ec4899"/>
            </linearGradient>
          </defs>
          <circle class="stp-09__track" cx="100" cy="100" r="90"/>
          <circle class="stp-09__arc" id="stp-09-arc" cx="100" cy="100" r="90"/>
        </svg>
        <div class="stp-09__ring-inner">
          <div class="stp-09__pct-big" id="stp-09-pct">25%</div>
          <div class="stp-09__pct-label">Complete</div>
          <div class="stp-09__step-count" id="stp-09-sc">Step 2 of 4</div>
        </div>
      </div>

      <!-- Linear pip row -->
      <div class="stp-09__track-row" id="stp-09-track">
        <div class="stp-09__pip done" data-i="1"><div class="stp-09__pnode">✓</div><span class="stp-09__plabel">Setup</span></div>
        <div class="stp-09__pip active" data-i="2"><div class="stp-09__pnode">2</div><span class="stp-09__plabel">Configure</span></div>
        <div class="stp-09__pip" data-i="3"><div class="stp-09__pnode">3</div><span class="stp-09__plabel">Integrate</span></div>
        <div class="stp-09__pip" data-i="4"><div class="stp-09__pnode">4</div><span class="stp-09__plabel">Launch</span></div>
      </div>
    </div>

    <!-- Step tiles grid -->
    <div class="stp-09__grid" id="stp-09-grid">
      <div class="stp-09__tile done" data-tile="1">
        <div class="stp-09__tile-step">Step 01</div>
        <div class="stp-09__tile-name">Project Setup</div>
        <div class="stp-09__tile-desc">Repository initialized, dependencies installed, CI configured.</div>
        <span class="stp-09__tile-badge">✓ Complete</span>
      </div>
      <div class="stp-09__tile active" data-tile="2">
        <div class="stp-09__tile-step">Step 02 · Active</div>
        <div class="stp-09__tile-name">Configuration</div>
        <div class="stp-09__tile-desc">Environment variables, feature flags, and API keys.</div>
        <span class="stp-09__tile-badge">● In Progress</span>
      </div>
      <div class="stp-09__tile" data-tile="3">
        <div class="stp-09__tile-step">Step 03</div>
        <div class="stp-09__tile-name">Integration</div>
        <div class="stp-09__tile-desc">Third-party services, webhooks, and data pipelines.</div>
        <span class="stp-09__tile-badge">Upcoming</span>
      </div>
      <div class="stp-09__tile" data-tile="4">
        <div class="stp-09__tile-step">Step 04</div>
        <div class="stp-09__tile-name">Launch</div>
        <div class="stp-09__tile-desc">Production deployment, monitoring, and rollout plan.</div>
        <span class="stp-09__tile-badge">Upcoming</span>
      </div>
    </div>

    <!-- Nav -->
    <div class="stp-09__nav">
      <button class="stp-09__btn stp-09__btn--ghost" id="stp-09-prev">← Prev</button>
      <button class="stp-09__btn stp-09__btn--primary" id="stp-09-next">Next Step →</button>
    </div>
  </div>
</div>
.stp-09,
.stp-09 *,
.stp-09 *::before,
.stp-09 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.stp-09 ::selection {
  background: #8b5cf6;
  color: #fff;
}

.stp-09 {
  --bg: #09090b;
  --card: #111113;
  --violet: #8b5cf6;
  --purple: #a78bfa;
  --pink: #ec4899;
  --white: #fafafa;
  --muted: #52525b;
  --border: #27272a;
  --success: #22c55e;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  background-image: radial-gradient(ellipse at 50% 50%,rgba(139,92,246,.04) 0%,transparent 70%);
}

.stp-09__shell {
  max-width: 680px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 48px;
}
/* big circular progress */

.stp-09__circle-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
}

.stp-09__ring {
  position: relative;
  width: 200px;
  height: 200px;
}

.stp-09__svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.stp-09__track {
  fill: none;
  stroke: var(--border);
  stroke-width: 8;
}

.stp-09__arc {
  fill: none;
  stroke-width: 8;
  stroke-linecap: round;
  stroke-dasharray: 565;
  stroke-dashoffset: 565;
  stroke: url(#stp-09-grad);
  transition: stroke-dashoffset .7s cubic-bezier(.4,0,.2,1);
}

.stp-09__ring-inner {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.stp-09__pct-big {
  font-size: 36px;
  font-weight: 800;
  color: var(--white);
  letter-spacing: -.04em;
  line-height: 1;
}

.stp-09__pct-label {
  font-size: 11px;
  color: var(--muted);
  letter-spacing: .1em;
  text-transform: uppercase;
  margin-top: 4px;
}

.stp-09__step-count {
  font-size: 13px;
  color: var(--violet);
  font-weight: 600;
  margin-top: 2px;
}
/* step pills below ring */

.stp-09__track-row {
  display: flex;
  align-items: center;
  gap: 0;
  width: 100%;
  max-width: 480px;
}

.stp-09__pip {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  position: relative;
}

.stp-09__pip:not(:last-child)::after {
  content: '';
  position: absolute;
  top: 12px;
  left: calc(50% + 12px);
  width: calc(100% - 24px);
  height: 2px;
  background: var(--border);
  transition: background .5s;
}

.stp-09__pip.done::after {
  background: linear-gradient(90deg,var(--violet),var(--purple));
}

.stp-09__pip.done .stp-09__pnode {
  background: linear-gradient(135deg,var(--violet),var(--purple));
  border-color: transparent;
  color: #fff;
}

.stp-09__pip.active .stp-09__pnode {
  border-color: var(--violet);
  color: var(--violet);
  box-shadow: 0 0 0 4px rgba(139,92,246,.15),0 0 16px rgba(139,92,246,.25);
}

.stp-09__pnode {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 700;
  border: 2px solid var(--border);
  background: var(--card);
  color: var(--muted);
  transition: all .35s cubic-bezier(.34,1.56,.64,1);
}

.stp-09__plabel {
  font-size: 10px;
  color: var(--muted);
  text-align: center;
  letter-spacing: .04em;
  transition: color .3s;
}

.stp-09__pip.done .stp-09__plabel {
  color: var(--purple);
}

.stp-09__pip.active .stp-09__plabel {
  color: var(--white);
}
/* content grid */

.stp-09__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  width: 100%;
}

.stp-09__tile {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 20px;
  transition: border-color .3s,box-shadow .3s;
  position: relative;
  overflow: hidden;
}

.stp-09__tile::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg,rgba(139,92,246,.05),transparent);
  opacity: 0;
  transition: opacity .3s;
}

.stp-09__tile.active {
  border-color: rgba(139,92,246,.4);
  box-shadow: 0 0 24px rgba(139,92,246,.08);
}

.stp-09__tile.active::before {
  opacity: 1;
}

.stp-09__tile.done {
  border-color: rgba(139,92,246,.15);
}

.stp-09__tile-step {
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 6px;
  font-weight: 600;
}

.stp-09__tile.done .stp-09__tile-step {
  color: var(--violet);
}

.stp-09__tile.active .stp-09__tile-step {
  color: var(--purple);
}

.stp-09__tile-name {
  font-size: 15px;
  font-weight: 700;
  color: var(--muted);
  transition: color .3s;
}

.stp-09__tile.done .stp-09__tile-name,
.stp-09__tile.active .stp-09__tile-name {
  color: var(--white);
}

.stp-09__tile-desc {
  font-size: 12px;
  color: var(--muted);
  margin-top: 4px;
  line-height: 1.5;
}

.stp-09__tile-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 10px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  background: rgba(139,92,246,.12);
  border: 1px solid rgba(139,92,246,.2);
  color: var(--violet);
}

.stp-09__tile.done .stp-09__tile-badge {
  background: rgba(34,197,94,.1);
  border-color: rgba(34,197,94,.2);
  color: var(--success);
}

.stp-09__tile.active .stp-09__tile-badge {
  background: rgba(139,92,246,.2);
  animation: stp-09-pulse 2s ease-in-out infinite;
}

@keyframes stp-09-pulse {
  0%,
    100% {
    opacity: 1;
  }

  50% {
    opacity: .6;
  }
}
/* nav */

.stp-09__nav {
  display: flex;
  gap: 12px;
  align-items: center;
}

.stp-09__btn {
  padding: 10px 24px;
  border-radius: 10px;
  border: none;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all .2s;
}

.stp-09__btn--ghost {
  background: var(--border);
  color: var(--muted);
}

.stp-09__btn--ghost:hover {
  color: var(--white);
}

.stp-09__btn--primary {
  background: linear-gradient(135deg,var(--violet),var(--purple));
  color: #fff;
  box-shadow: 0 4px 16px rgba(139,92,246,.35);
}

.stp-09__btn--primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 24px rgba(139,92,246,.45);
}

@media (prefers-reduced-motion: reduce) {
  .stp-09__arc {
    transition: none;
  }

  .stp-09__tile.active .stp-09__tile-badge {
    animation: none;
  }
}
(function(){
  let cur=2;
  const total=4;
  const pips=document.querySelectorAll('.stp-09__pip');
  const tiles=document.querySelectorAll('.stp-09__tile');
  const arc=document.getElementById('stp-09-arc');
  const pctEl=document.getElementById('stp-09-pct');
  const scEl=document.getElementById('stp-09-sc');
  const circumference=565;
  const badgeTexts=['✓ Complete','● In Progress','Upcoming','Upcoming'];
  const badgeClasses=['done','active','',''];

  function update(){
    // ring
    const pct=(cur-1)/(total-1);
    arc.style.strokeDashoffset=circumference-(circumference*pct);
    pctEl.textContent=Math.round(pct*100)+'%';
    scEl.textContent=`Step ${cur} of ${total}`;

    // pips
    pips.forEach((p,i)=>{
      p.classList.remove('done','active');
      const node=p.querySelector('.stp-09__pnode');
      if(i+1<cur){p.classList.add('done');node.textContent='✓';}
      else if(i+1===cur){p.classList.add('active');node.textContent=cur;}
      else node.textContent=i+1;
    });

    // tiles
    tiles.forEach((t,i)=>{
      t.classList.remove('done','active');
      const badge=t.querySelector('.stp-09__tile-badge');
      const step=t.querySelector('.stp-09__tile-step');
      if(i+1<cur){t.classList.add('done');badge.textContent='✓ Complete';step.textContent=`Step 0${i+1}`;}
      else if(i+1===cur){t.classList.add('active');badge.textContent='● In Progress';step.textContent=`Step 0${i+1} · Active`;}
      else{badge.textContent='Upcoming';step.textContent=`Step 0${i+1}`;}
    });
  }

  document.getElementById('stp-09-prev').addEventListener('click',()=>{cur=Math.max(1,cur-1);update();});
  document.getElementById('stp-09-next').addEventListener('click',()=>{cur=Math.min(total,cur+1);update();});
  update();
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Stepper from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Circular Step Progress Indicator
Source: https://codefronts.com/components/css-stepper-designs/css-circular-step-progress-indicator/

A centred SVG arc ring whose stroke-dashoffset advances per step, surrounded by a tile grid of clickable step cards — violet/purple accent on a near-black background ideal for dashboard headers.
## HTML
```html
<div class="stp-09">
  <div class="stp-09__shell">

    <!-- Big circular ring -->
    <div class="stp-09__circle-wrap">
      <div class="stp-09__ring">
        <svg class="stp-09__svg" viewBox="0 0 200 200">
          <defs>
            <linearGradient id="stp-09-grad" x1="0%" y1="0%" x2="100%" y2="100%">
              <stop offset="0%" stop-color="#8b5cf6"/>
              <stop offset="100%" stop-color="#ec4899"/>
            </linearGradient>
          </defs>
          <circle class="stp-09__track" cx="100" cy="100" r="90"/>
          <circle class="stp-09__arc" id="stp-09-arc" cx="100" cy="100" r="90"/>
        </svg>
        <div class="stp-09__ring-inner">
          <div class="stp-09__pct-big" id="stp-09-pct">25%</div>
          <div class="stp-09__pct-label">Complete</div>
          <div class="stp-09__step-count" id="stp-09-sc">Step 2 of 4</div>
        </div>
      </div>

      <!-- Linear pip row -->
      <div class="stp-09__track-row" id="stp-09-track">
        <div class="stp-09__pip done" data-i="1"><div class="stp-09__pnode">✓</div><span class="stp-09__plabel">Setup</span></div>
        <div class="stp-09__pip active" data-i="2"><div class="stp-09__pnode">2</div><span class="stp-09__plabel">Configure</span></div>
        <div class="stp-09__pip" data-i="3"><div class="stp-09__pnode">3</div><span class="stp-09__plabel">Integrate</span></div>
        <div class="stp-09__pip" data-i="4"><div class="stp-09__pnode">4</div><span class="stp-09__plabel">Launch</span></div>
      </div>
    </div>

    <!-- Step tiles grid -->
    <div class="stp-09__grid" id="stp-09-grid">
      <div class="stp-09__tile done" data-tile="1">
        <div class="stp-09__tile-step">Step 01</div>
        <div class="stp-09__tile-name">Project Setup</div>
        <div class="stp-09__tile-desc">Repository initialized, dependencies installed, CI configured.</div>
        <span class="stp-09__tile-badge">✓ Complete</span>
      </div>
      <div class="stp-09__tile active" data-tile="2">
        <div class="stp-09__tile-step">Step 02 · Active</div>
        <div class="stp-09__tile-name">Configuration</div>
        <div class="stp-09__tile-desc">Environment variables, feature flags, and API keys.</div>
        <span class="stp-09__tile-badge">● In Progress</span>
      </div>
      <div class="stp-09__tile" data-tile="3">
        <div class="stp-09__tile-step">Step 03</div>
        <div class="stp-09__tile-name">Integration</div>
        <div class="stp-09__tile-desc">Third-party services, webhooks, and data pipelines.</div>
        <span class="stp-09__tile-badge">Upcoming</span>
      </div>
      <div class="stp-09__tile" data-tile="4">
        <div class="stp-09__tile-step">Step 04</div>
        <div class="stp-09__tile-name">Launch</div>
        <div class="stp-09__tile-desc">Production deployment, monitoring, and rollout plan.</div>
        <span class="stp-09__tile-badge">Upcoming</span>
      </div>
    </div>

    <!-- Nav -->
    <div class="stp-09__nav">
      <button class="stp-09__btn stp-09__btn--ghost" id="stp-09-prev">← Prev</button>
      <button class="stp-09__btn stp-09__btn--primary" id="stp-09-next">Next Step →</button>
    </div>
  </div>
</div>
```
## CSS
```css
.stp-09,
.stp-09 *,
.stp-09 *::before,
.stp-09 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.stp-09 ::selection {
  background: #8b5cf6;
  color: #fff;
}

.stp-09 {
  --bg: #09090b;
  --card: #111113;
  --violet: #8b5cf6;
  --purple: #a78bfa;
  --pink: #ec4899;
  --white: #fafafa;
  --muted: #52525b;
  --border: #27272a;
  --success: #22c55e;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  background-image: radial-gradient(ellipse at 50% 50%,rgba(139,92,246,.04) 0%,transparent 70%);
}

.stp-09__shell {
  max-width: 680px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 48px;
}
/* big circular progress */

.stp-09__circle-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
}

.stp-09__ring {
  position: relative;
  width: 200px;
  height: 200px;
}

.stp-09__svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.stp-09__track {
  fill: none;
  stroke: var(--border);
  stroke-width: 8;
}

.stp-09__arc {
  fill: none;
  stroke-width: 8;
  stroke-linecap: round;
  stroke-dasharray: 565;
  stroke-dashoffset: 565;
  stroke: url(#stp-09-grad);
  transition: stroke-dashoffset .7s cubic-bezier(.4,0,.2,1);
}

.stp-09__ring-inner {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.stp-09__pct-big {
  font-size: 36px;
  font-weight: 800;
  color: var(--white);
  letter-spacing: -.04em;
  line-height: 1;
}

.stp-09__pct-label {
  font-size: 11px;
  color: var(--muted);
  letter-spacing: .1em;
  text-transform: uppercase;
  margin-top: 4px;
}

.stp-09__step-count {
  font-size: 13px;
  color: var(--violet);
  font-weight: 600;
  margin-top: 2px;
}
/* step pills below ring */

.stp-09__track-row {
  display: flex;
  align-items: center;
  gap: 0;
  width: 100%;
  max-width: 480px;
}

.stp-09__pip {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  position: relative;
}

.stp-09__pip:not(:last-child)::after {
  content: '';
  position: absolute;
  top: 12px;
  left: calc(50% + 12px);
  width: calc(100% - 24px);
  height: 2px;
  background: var(--border);
  transition: background .5s;
}

.stp-09__pip.done::after {
  background: linear-gradient(90deg,var(--violet),var(--purple));
}

.stp-09__pip.done .stp-09__pnode {
  background: linear-gradient(135deg,var(--violet),var(--purple));
  border-color: transparent;
  color: #fff;
}

.stp-09__pip.active .stp-09__pnode {
  border-color: var(--violet);
  color: var(--violet);
  box-shadow: 0 0 0 4px rgba(139,92,246,.15),0 0 16px rgba(139,92,246,.25);
}

.stp-09__pnode {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 700;
  border: 2px solid var(--border);
  background: var(--card);
  color: var(--muted);
  transition: all .35s cubic-bezier(.34,1.56,.64,1);
}

.stp-09__plabel {
  font-size: 10px;
  color: var(--muted);
  text-align: center;
  letter-spacing: .04em;
  transition: color .3s;
}

.stp-09__pip.done .stp-09__plabel {
  color: var(--purple);
}

.stp-09__pip.active .stp-09__plabel {
  color: var(--white);
}
/* content grid */

.stp-09__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  width: 100%;
}

.stp-09__tile {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 20px;
  transition: border-color .3s,box-shadow .3s;
  position: relative;
  overflow: hidden;
}

.stp-09__tile::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg,rgba(139,92,246,.05),transparent);
  opacity: 0;
  transition: opacity .3s;
}

.stp-09__tile.active {
  border-color: rgba(139,92,246,.4);
  box-shadow: 0 0 24px rgba(139,92,246,.08);
}

.stp-09__tile.active::before {
  opacity: 1;
}

.stp-09__tile.done {
  border-color: rgba(139,92,246,.15);
}

.stp-09__tile-step {
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 6px;
  font-weight: 600;
}

.stp-09__tile.done .stp-09__tile-step {
  color: var(--violet);
}

.stp-09__tile.active .stp-09__tile-step {
  color: var(--purple);
}

.stp-09__tile-name {
  font-size: 15px;
  font-weight: 700;
  color: var(--muted);
  transition: color .3s;
}

.stp-09__tile.done .stp-09__tile-name,
.stp-09__tile.active .stp-09__tile-name {
  color: var(--white);
}

.stp-09__tile-desc {
  font-size: 12px;
  color: var(--muted);
  margin-top: 4px;
  line-height: 1.5;
}

.stp-09__tile-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 10px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  background: rgba(139,92,246,.12);
  border: 1px solid rgba(139,92,246,.2);
  color: var(--violet);
}

.stp-09__tile.done .stp-09__tile-badge {
  background: rgba(34,197,94,.1);
  border-color: rgba(34,197,94,.2);
  color: var(--success);
}

.stp-09__tile.active .stp-09__tile-badge {
  background: rgba(139,92,246,.2);
  animation: stp-09-pulse 2s ease-in-out infinite;
}

@keyframes stp-09-pulse {
  0%,
    100% {
    opacity: 1;
  }

  50% {
    opacity: .6;
  }
}
/* nav */

.stp-09__nav {
  display: flex;
  gap: 12px;
  align-items: center;
}

.stp-09__btn {
  padding: 10px 24px;
  border-radius: 10px;
  border: none;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all .2s;
}

.stp-09__btn--ghost {
  background: var(--border);
  color: var(--muted);
}

.stp-09__btn--ghost:hover {
  color: var(--white);
}

.stp-09__btn--primary {
  background: linear-gradient(135deg,var(--violet),var(--purple));
  color: #fff;
  box-shadow: 0 4px 16px rgba(139,92,246,.35);
}

.stp-09__btn--primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 24px rgba(139,92,246,.45);
}

@media (prefers-reduced-motion: reduce) {
  .stp-09__arc {
    transition: none;
  }

  .stp-09__tile.active .stp-09__tile-badge {
    animation: none;
  }
}
```

## JavaScript
```js
(function(){
  let cur=2;
  const total=4;
  const pips=document.querySelectorAll('.stp-09__pip');
  const tiles=document.querySelectorAll('.stp-09__tile');
  const arc=document.getElementById('stp-09-arc');
  const pctEl=document.getElementById('stp-09-pct');
  const scEl=document.getElementById('stp-09-sc');
  const circumference=565;
  const badgeTexts=['✓ Complete','● In Progress','Upcoming','Upcoming'];
  const badgeClasses=['done','active','',''];

  function update(){
    // ring
    const pct=(cur-1)/(total-1);
    arc.style.strokeDashoffset=circumference-(circumference*pct);
    pctEl.textContent=Math.round(pct*100)+'%';
    scEl.textContent=`Step ${cur} of ${total}`;

    // pips
    pips.forEach((p,i)=>{
      p.classList.remove('done','active');
      const node=p.querySelector('.stp-09__pnode');
      if(i+1<cur){p.classList.add('done');node.textContent='✓';}
      else if(i+1===cur){p.classList.add('active');node.textContent=cur;}
      else node.textContent=i+1;
    });

    // tiles
    tiles.forEach((t,i)=>{
      t.classList.remove('done','active');
      const badge=t.querySelector('.stp-09__tile-badge');
      const step=t.querySelector('.stp-09__tile-step');
      if(i+1<cur){t.classList.add('done');badge.textContent='✓ Complete';step.textContent=`Step 0${i+1}`;}
      else if(i+1===cur){t.classList.add('active');badge.textContent='● In Progress';step.textContent=`Step 0${i+1} · Active`;}
      else{badge.textContent='Upcoming';step.textContent=`Step 0${i+1}`;}
    });
  }

  document.getElementById('stp-09-prev').addEventListener('click',()=>{cur=Math.max(1,cur-1);update();});
  document.getElementById('stp-09-next').addEventListener('click',()=>{cur=Math.min(total,cur+1);update();});
  update();
})();
```

How this works

The ring is an SVG <circle> with stroke-dasharray equal to its circumference (2πr). JS calculates progress as (cur - 1) / total and sets stroke-dashoffset to circumference * (1 - progress), filling the ring clockwise. A -90deg CSS rotation on the SVG starts the fill at 12 o'clock.

Below the ring, step cards are arranged in a responsive CSS Grid. Each card has a coloured top border that switches to the accent colour when active, and a checkmark badge when done. Clicking a card navigates directly to that step — enabling non-linear navigation unlike purely sequential steppers.

Make it yours

  • Change the ring radius by editing the r attribute on the SVG circle and recalculating stroke-dasharray = 2 * Math.PI * r in the JS constant.
  • Edit --violet and --purple at .stp-09 to retheme the ring stroke, active cards, and progress text.
  • Add a percentage label inside the ring by absolutely positioning a text element centred over the SVG.
  • Swap the tile grid for a horizontal tab bar by changing the grid to a single-row flex layout with flex-wrap:nowrap.
  • Add a smooth ring animation by setting transition: stroke-dashoffset .6s cubic-bezier(.4,0,.2,1) on the circle in CSS.

Gotchas — read before shipping

  • SVG stroke-dashoffset only animates if a CSS transition is set — the property is not transitioned by default unlike opacity or transform.
  • The ring starts at the 3 o'clock position natively — the rotate(-90deg) on the SVG is essential; removing it breaks the 12 o'clock start.
  • Direct step navigation via tile click can skip mandatory steps — add a guard that prevents forward jumps past incomplete steps if required by the flow.

Browser support

ChromeSafariFirefoxEdge
88+14+78+88+

SVG stroke animation supported universally; CSS rotate shorthand requires Chrome 104+ / Safari 14.1+.

Techniques used in this demo

Search CodeFronts

Loading…