@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));
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));
})();