Stats

By the numbers

Trusted by teams around the world

Developers
12000+
use Spruce every month
Uptime
99.9%
for the hosted docs and CDN
Core size
9 KB
gzipped, with everything you need
Design styles
8
built in, and you can add more
@use 'sprucecss/scss/spruce' as *;

.stats-block {
  padding-block: spacer-clamp('xl', 'xxl');

  &__inner {
    display: flex;
    flex-direction: column;
    gap: spacer('l');
  }

  &--brand {
    @include set-css-variable((
      --base-color-heading: color('primary-foreground', 'btn'),
      --base-color-muted: color-alpha('primary-foreground', 0.8, 'btn'),
      --base-color-primary: color('primary-foreground', 'btn'),
      --base-color-text: color('primary-foreground', 'btn'),
    ));
    @include selection(color('secondary-background', 'btn'), color('secondary-foreground', 'btn'));
    background-color: color('primary-background', 'btn');
    background-image: radial-gradient(circle at 15% 0%, #{color-alpha('primary-foreground', 0.15, 'btn')}, transparent 50%);
    color: color('primary-foreground', 'btn');
  }

  &__grid {
    display: grid;
    gap: spacer('m');
    grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
    // A child of the flex __inner: the gap spaces it, so drop the browser margins.
    margin-block: 0;
  }

  &__item {
    border-inline-start: 2px solid color-alpha('primary', 0.4);
    display: flex;
    flex-direction: column;
    padding-inline-start: spacer('m');
  }

  &__label {
    color: color('heading');
    font-weight: 700;
    order: 2;
  }

  &__value {
    color: color('heading');
    font-family: config('font-family-heading', $typography);
    font-size: responsive-font-size(3rem, 30, '3vw + 1rem');
    font-weight: config('font-weight-heading', $typography);
    line-height: 1.1;
    margin: 0;
    order: 1;
  }

  &__text {
    color: color('muted');
    margin: 0;
    order: 3;
  }
}
<section class="stats-block stats-block--brand" aria-labelledby="stats-title">
    <div class="container">
        <div class="stats-block__inner">
            <header class="block-heading">
                <p class="block-heading__eyebrow">By the numbers</p>
                <h2 class="block-heading__title" id="stats-title">Trusted by teams around the world</h2>
            </header>
            <dl class="stats-block__grid">
                <div class="stats-block__item">
                    <dt class="stats-block__label">Developers</dt>
                    <dd class="stats-block__value"><span data-count-to="12000">12000</span>+</dd>
                    <dd class="stats-block__text">use Spruce every month</dd>
                </div>
                <div class="stats-block__item">
                    <dt class="stats-block__label">Uptime</dt>
                    <dd class="stats-block__value"><span data-count-to="99.9">99.9</span>%</dd>
                    <dd class="stats-block__text">for the hosted docs and CDN</dd>
                </div>
                <div class="stats-block__item">
                    <dt class="stats-block__label">Core size</dt>
                    <dd class="stats-block__value"><span data-count-to="9">9</span> KB</dd>
                    <dd class="stats-block__text">gzipped, with everything you need</dd>
                </div>
                <div class="stats-block__item">
                    <dt class="stats-block__label">Design styles</dt>
                    <dd class="stats-block__value"><span data-count-to="8">8</span></dd>
                    <dd class="stats-block__text">built in, and you can add more</dd>
                </div>
            </dl>
        </div>
    </div>
</section>
(() => {
  const counters = document.querySelectorAll('[data-count-to]');
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  if (!counters.length || reduceMotion) {
    return;
  }

  const format = (value, decimals) => value.toLocaleString(undefined, { maximumFractionDigits: decimals, minimumFractionDigits: decimals });

  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) {
        return;
      }

      const element = entry.target;
      const target = Number(element.dataset.countTo);
      const decimals = (element.dataset.countTo.split('.')[1] || '').length;
      const start = performance.now();
      const duration = 1200;

      const tick = (now) => {
        const progress = Math.min(Math.max((now - start) / duration, 0), 1);
        const eased = 1 - Math.pow(1 - progress, 3);

        element.textContent = format(target * eased, decimals);

        if (progress < 1) {
          requestAnimationFrame(tick);
        }
      };

      requestAnimationFrame(tick);
      observer.unobserve(element);
    });
  }, { threshold: 0.5 });

  counters.forEach((counter) => observer.observe(counter));
})();

Stats

On this page

A statistics band with big animated numbers. The brand modifier turns it into a full-width colored block.

Technical Details

  • The numbers are a description list: the label is the term, the value and the explanation are the descriptions. The visual order (value first) is set with CSS order, so the reading order stays meaningful.
  • The script counts the numbers up when the block scrolls into view, only if the user doesn't prefer reduced motion. Without JavaScript, the final numbers are shown.
  • .stats-block--brand uses the primary button colors as the background and redefines the text color tokens for its subtree, so the Block Heading inside it adapts automatically.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.