← Component Library
Visual Design Components

Visual Design Patterns

Eye-catching ways to show numbers and progress — stat counters and circular indicators. Each with a live demo and the HTML & CSS.

01 · Statistics Counter

Show off your numbers

A row of big numbers with short labels — great for landing pages (“10k users, 99.9% uptime”). A responsive grid keeps them evenly spaced.

12k+
Active users
99.9%
Uptime
4.9★
Avg. rating
36
Countries

HTML

<div class="stats">
  <div class="stat"><div class="num" data-target="12000" data-suffix="+">0</div><div class="lbl">Active users</div></div>
  <div class="stat"><div class="num" data-target="99.9" data-suffix="%">0</div><div class="lbl">Uptime</div></div>
  <div class="stat"><div class="num" data-target="4.9" data-suffix="★">0</div><div class="lbl">Avg. rating</div></div>
  <div class="stat"><div class="num" data-target="36">0</div><div class="lbl">Countries</div></div>
</div>

CSS

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 18px; text-align: center;
}
.stat .num { font-size: 2.2rem; font-weight: 800; color: #38bdf8; line-height: 1; }
.stat .lbl { font-size: .82rem; color: #6b7280; font-weight: 600; margin-top: 6px; }

JavaScript — count up when scrolled into view

// Animates every .num[data-target] from 0 to its target when it scrolls into view.
// Uses IntersectionObserver to trigger once, then requestAnimationFrame to animate.
const counters = document.querySelectorAll('.stat .num[data-target]');

function format(n, target) {
  const decimals = String(target).indexOf('.') > -1 ? 1 : 0;
  if (decimals) return n.toFixed(1);
  return Math.round(n) >= 1000
    ? (Math.round(n) / 1000).toFixed(0) + 'k'
    : Math.round(n).toString();
}

function animate(el) {
  const target = parseFloat(el.dataset.target);
  const suffix = el.dataset.suffix || '';
  const duration = 1400;
  let start = null;
  function step(t) {
    if (start === null) start = t;
    const p = Math.min((t - start) / duration, 1);
    const eased = 1 - Math.pow(1 - p, 3);   // easeOutCubic
    el.textContent = format(target * eased, target) + suffix;
    if (p < 1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

const io = new IntersectionObserver((entries) => {
  entries.forEach((e) => {
    if (e.isIntersecting) {
      animate(e.target);
      io.unobserve(e.target);
    }
  });
}, { threshold: 0.4 });

counters.forEach((c) => io.observe(c));
The data-target attribute on each .num tells the script the final value. Strings like "12k+" become numbers (12000) plus an optional data-suffix ("+") appended after.
02 · Circular Progress Indicator

A progress ring — no SVG needed

A single conic-gradient draws the ring; a smaller circle on top punches the hole. Change one custom property (--p) to set the percentage.

25%
60%
90%

HTML

<div class="rings">
  <div class="ring" style="--p:0" data-percent="25"><span>0%</span></div>
  <div class="ring" style="--p:0" data-percent="60"><span>0%</span></div>
  <div class="ring" style="--p:0" data-percent="90"><span>0%</span></div>
</div>

CSS

.rings { display: flex; gap: 26px; flex-wrap: wrap; align-items: center; }
.ring {
  width: 104px; height: 104px; border-radius: 50%;
  display: grid; place-items: center;
  background: conic-gradient(#38bdf8 calc(var(--p) * 1%), #e5e7eb 0);
  transition: --p 1.2s ease;            /* needs @property for browsers that support it */
}
.ring span {                            /* center hole + label */
  width: 78px; height: 78px; border-radius: 50%;
  background: #fff; display: grid; place-items: center;
  font-weight: 800; color: #111; font-size: 1.1rem;
}
@property --p { syntax: '<number>'; inherits: false; initial-value: 0; }

JavaScript — animate the ring fill on scroll-into-view

// Animates each .ring's --p from 0 to data-percent when scrolled into view.
// Updates both the conic-gradient (via the CSS variable) and the label.
const rings = document.querySelectorAll('.ring[data-percent]');

function animateRing(ring) {
  const target = parseFloat(ring.dataset.percent);
  const label = ring.querySelector('span');
  const duration = 1200;
  let start = null;
  function step(t) {
    if (start === null) start = t;
    const p = Math.min((t - start) / duration, 1);
    const eased = 1 - Math.pow(1 - p, 3);
    const current = target * eased;
    ring.style.setProperty('--p', current);
    if (label) label.textContent = Math.round(current) + '%';
    if (p < 1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

const ringObserver = new IntersectionObserver((entries) => {
  entries.forEach((e) => {
    if (e.isIntersecting) {
      animateRing(e.target);
      ringObserver.unobserve(e.target);
    }
  });
}, { threshold: 0.5 });

rings.forEach((r) => ringObserver.observe(r));
For a smooth animated sweep or rounded ends, an <svg> circle with stroke-dasharray gives you more control. The JS above works with the pure-CSS conic-gradient version by tweening the --p custom property.
03 · Progress Bar

A linear bar that fills to a value

The horizontal counterpart to the ring above — a track with a coloured fill whose width is the percentage. Use it for uploads, completion, or any 0–100 value. Add a stripe or gradient for a richer look.

Uploading…35%
Profile complete80%

HTML

<div style="max-width:420px;display:grid;gap:16px;">
  <div>
    <div style="display:flex;justify-content:space-between;font-size:.8rem;color:#374151;margin-bottom:5px;"><span>Uploading…</span><span>35%</span></div>
    <div class="bar"><i data-progress="35" style="width:0%;"></i></div>
  </div>
  <div>
    <div style="display:flex;justify-content:space-between;font-size:.8rem;color:#374151;margin-bottom:5px;"><span>Profile complete</span><span>80%</span></div>
    <div class="bar"><i class="grad" data-progress="80" style="width:0%;"></i></div>
  </div>
</div>

CSS

.bar { background: #e5e7eb; border-radius: 999px; height: 12px; overflow: hidden; }
.bar i { display: block; height: 100%; border-radius: 999px;
         background: #38bdf8; transition: width 1s ease; }
.bar i.grad { background: linear-gradient(90deg, #38bdf8, #22c55e); }

JavaScript — fill the bar when it scrolls into view

// Animates each .bar i from 0% to its data-progress value on scroll-into-view.
// The CSS transition does the actual animation; JS just flips the width.
const fills = document.querySelectorAll('.bar i[data-progress]');

const barObserver = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const fill = entry.target;
      const target = fill.dataset.progress + '%';
      // requestAnimationFrame so the transition actually fires from 0%
      requestAnimationFrame(() => { fill.style.width = target; });
      barObserver.unobserve(fill);
    }
  });
}, { threshold: 0.5 });

fills.forEach((f) => barObserver.observe(f));
For semantics and free screen-reader support, the native <progress value="80" max="100"></progress> element works too — style it with ::-webkit-progress-value.
04 · Service Card

Present what you offer

A service card pairs an icon, a service name, a short description, and a link — the “What we do” row on most business sites. A responsive grid lays out three or more; a subtle lift on hover adds polish.

Web Design

Clean, responsive layouts built to match your brand.

Learn more

Development

Fast, accessible sites coded with modern HTML & CSS.

Learn more

Launch & SEO

We deploy, optimize, and help people find you.

Learn more

HTML

<div class="services">
  <div class="service">
    <div class="icon">✏</div>
    <h3>Web Design</h3>
    <p>Clean, responsive layouts built to match your brand.</p>
    <a href="/services/design">Learn more →</a>
  </div>
  <div class="service">
    <div class="icon" style="background:#ede9fe;color:#7c3aed;">💻</div>
    <h3>Development</h3>
    <p>Fast, accessible sites coded with modern HTML & CSS.</p>
    <a href="/services/development">Learn more →</a>
  </div>
  <div class="service">
    <div class="icon" style="background:#dcfce7;color:#16a34a;">🚀</div>
    <h3>Launch & SEO</h3>
    <p>We deploy, optimize, and help people find you.</p>
    <a href="/services/launch">Learn more →</a>
  </div>
</div>

CSS

.services { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.service  { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 22px;
            transition: transform .2s, box-shadow .2s; }
.service:hover { transform: translateY(-3px); box-shadow: 0 10px 24px rgba(0,0,0,.08); }  /* lift */
.service .icon { width: 52px; height: 52px; border-radius: 14px; display: grid; place-items: center;
            background: #e0f2fe; color: #0284c7; font-size: 1.6rem; margin-bottom: 14px; }
.service h3 { color: #111; font-size: 1.05rem; margin-bottom: 6px; }
.service p  { color: #6b7280; font-size: .86rem; margin-bottom: 12px; }
.service a  { color: #0284c7; font-weight: 700; font-size: .84rem; text-decoration: none; }

When to use: a “Services,” “Features,” or “What we offer” section. Keep each card to one icon, one heading, one sentence, and one link.

05 · Full Page

The whole thing as one file

Everything above — stat counters, a progress ring, progress bars, and service cards — combined into one complete HTML file. Copy it into a new file called index.html, open it in a browser, and it just works — no libraries, no build step. The stats even count up on load. Here's the live result, then the full code.

Live preview

Complete HTML — copy this whole file

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Visual Design Patterns</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
           color: #1f2433; background: #f6f7fb; line-height: 1.6; padding: 32px 20px; }
    .page { max-width: 880px; margin: 0 auto; display: grid; gap: 32px; }
    .page h1 { font-size: 2rem; letter-spacing: -.02em; }
    .page h2 { font-size: 1.1rem; margin-bottom: 16px; color: #111; }
    .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 16px; padding: 26px 24px; }

    /* ===== Statistics counter ===== */
    .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
             gap: 18px; text-align: center; }
    .stat .num { font-size: 2.2rem; font-weight: 800; color: #38bdf8; line-height: 1; }
    .stat .lbl { font-size: .82rem; color: #6b7280; font-weight: 600; margin-top: 6px; }

    /* ===== Circular progress ring ===== */
    .rings { display: flex; gap: 26px; flex-wrap: wrap; align-items: center; }
    .ring { width: 104px; height: 104px; border-radius: 50%;
            display: grid; place-items: center;
            background: conic-gradient(#38bdf8 calc(var(--p) * 1%), #e5e7eb 0); }
    .ring span { width: 78px; height: 78px; border-radius: 50%;
                 background: #fff; display: grid; place-items: center;
                 font-weight: 800; color: #111; font-size: 1.1rem; }

    /* ===== Progress bars ===== */
    .bars { max-width: 460px; display: grid; gap: 16px; }
    .bar-row { display: flex; justify-content: space-between; font-size: .8rem;
               color: #374151; margin-bottom: 5px; }
    .bar { background: #e5e7eb; border-radius: 999px; height: 12px; overflow: hidden; }
    .bar i { display: block; height: 100%; border-radius: 999px;
             background: #38bdf8; transition: width .8s ease; }
    .bar i.grad { background: linear-gradient(90deg, #38bdf8, #22c55e); }

    /* ===== Service cards ===== */
    .services { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
    .service { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 22px;
               transition: transform .2s, box-shadow .2s; }
    .service:hover { transform: translateY(-3px); box-shadow: 0 10px 24px rgba(0,0,0,.08); }
    .service .icon { width: 52px; height: 52px; border-radius: 14px; display: grid; place-items: center;
                     background: #e0f2fe; color: #0284c7; font-size: 1.6rem; margin-bottom: 14px; }
    .service h3 { color: #111; font-size: 1.05rem; margin-bottom: 6px; }
    .service p { color: #6b7280; font-size: .86rem; margin-bottom: 12px; }
    .service a { color: #0284c7; font-weight: 700; font-size: .84rem; text-decoration: none; }
    .service a:hover { text-decoration: underline; }
  </style>
</head>
<body>
  <div class="page">
    <h1>Visual Design Patterns</h1>

    <section class="card">
      <h2>By the numbers</h2>
      <div class="stats">
        <div class="stat"><div class="num" data-to="12000" data-suffix="+">0</div><div class="lbl">Active users</div></div>
        <div class="stat"><div class="num" data-to="99" data-suffix="%">0</div><div class="lbl">Uptime</div></div>
        <div class="stat"><div class="num" data-to="36">0</div><div class="lbl">Countries</div></div>
        <div class="stat"><div class="num" data-to="4.9">0</div><div class="lbl">Avg. rating</div></div>
      </div>
    </section>

    <section class="card">
      <h2>Progress, two ways</h2>
      <div class="rings">
        <div class="ring" style="--p:25"><span>25%</span></div>
        <div class="ring" style="--p:60"><span>60%</span></div>
        <div class="ring" style="--p:90"><span>90%</span></div>
      </div>
      <div class="bars" style="margin-top:24px;">
        <div>
          <div class="bar-row"><span>Uploading…</span><span>35%</span></div>
          <div class="bar"><i style="width:0" data-w="35%"></i></div>
        </div>
        <div>
          <div class="bar-row"><span>Profile complete</span><span>80%</span></div>
          <div class="bar"><i class="grad" style="width:0" data-w="80%"></i></div>
        </div>
      </div>
    </section>

    <section class="card">
      <h2>What we offer</h2>
      <div class="services">
        <div class="service">
          <div class="icon">✏</div>
          <h3>Web Design</h3>
          <p>Clean, responsive layouts built to match your brand.</p>
          <a href="#">Learn more →</a>
        </div>
        <div class="service">
          <div class="icon" style="background:#ede9fe;color:#7c3aed;">💻</div>
          <h3>Development</h3>
          <p>Fast, accessible sites coded with modern HTML & CSS.</p>
          <a href="#">Learn more →</a>
        </div>
        <div class="service">
          <div class="icon" style="background:#dcfce7;color:#16a34a;">🚀</div>
          <h3>Launch & SEO</h3>
          <p>We deploy, optimize, and help people find you.</p>
          <a href="#">Learn more →</a>
        </div>
      </div>
    </section>
  </div>

  <script>
    /* Count the stat numbers up from 0 on load */
    document.querySelectorAll('.num[data-to]').forEach(function (el) {
      var to = parseFloat(el.dataset.to);
      var suffix = el.dataset.suffix || '';
      var decimals = (el.dataset.to.indexOf('.') > -1) ? 1 : 0;
      var start = null, dur = 1400;
      function fmt(n) {
        if (decimals) return n.toFixed(1);
        return Math.round(n) >= 1000 ? Math.round(n / 1000) + 'k' : Math.round(n).toString();
      }
      function step(t) {
        if (start === null) start = t;
        var p = Math.min((t - start) / dur, 1);
        var eased = 1 - Math.pow(1 - p, 3);
        el.textContent = fmt(to * eased) + suffix;
        if (p < 1) requestAnimationFrame(step);
      }
      requestAnimationFrame(step);
    });

    /* Fill the progress bars on load */
    window.addEventListener('load', function () {
      document.querySelectorAll('.bar i[data-w]').forEach(function (fill) {
        requestAnimationFrame(function () { fill.style.width = fill.dataset.w; });
      });
    });
  </script>
</body>
</html>
That's all four patterns in one file. The numbers count up and the bars fill on load — remove the <script> for static values. Keep the class names (.stats, .ring, .bar, .service) as-is or rename them in both the markup and the <style>.
06 · Related

Keep building

More visual pieces in Cards Lab, Hero Lab, and Dashboard Lab — or browse the full Component Library.