why-choose-us-counters
Статистика·Шаблон: Consulo Creative Business Consulting Template·Складність анімації: medium·Адаптивний: Так

Файли-джерела
- index.html
main .promotion.mt-100
Бібліотеки
aosbootstrap
Summary
Dark "Why Choose Us" promotion band combining a 7/12 narrative column (eyebrow + headline + paragraph + CTA) with a <counter-up> stats strip animating from 0 to target on intersection. The whole section binds --color-background to graphite so all child text inverts to white.
HTML structure (minimal)
<div class="promotion mt-100 section-padding">
<div class="container">
<div class="promotion-container">
<div class="row">
<div class="col-lg-7 col-12">
<div class="promtion-content section-headings">
<div class="subheading text-20 subheading-bg" data-aos="fade-right"><span>Why Choose Us</span></div>
<h2 class="heading text-50" data-aos="fade-right">Smart strategies for sustainable business growth</h2>
<div class="text text-18" data-aos="fade-right">Description paragraph…</div>
<a href="about.html" class="button button--secondary">Learn More</a>
</div>
</div>
<div class="col-lg-5 col-12"><!-- imagery / illustration --></div>
</div>
<counter-up class="counter-list">
<div class="counter-item" data-aos="fade-up">
<h2 class="heading text-50" data-target="500">0<span>+</span></h2>
<div class="text text-18 fw-500">Happy clients</div>
</div>
<div class="counter-item" data-aos="fade-up" data-aos-delay="100">
<h2 class="heading text-50" data-target="120">0<span>+</span></h2>
<div class="text text-18 fw-500">Projects done</div>
</div>
<div class="counter-item" data-aos="fade-up" data-aos-delay="200">
<h2 class="heading text-50" data-target="25">0<span>+</span></h2>
<div class="text text-18 fw-500">Years experiences</div>
</div>
</counter-up>
</div>
</div>
</div>
Key SCSS tokens
.promotion {
--color-background: rgba(32,40,45,1);
--color-foreground: rgba(255,255,255,1);
--color-foreground-heading: rgba(255,255,255,1);
--color-foreground-subheading: rgba(255,255,255,1);
}
.promotion-container {
background-color: var(--color-background);
border-radius: 18px;
padding: 80px;
border: 1px solid var(--color-border);
}
.counter-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
margin-block-start: 60px;
}
.counter-item .heading {
font-size: 50px;
font-weight: 700;
}
Animation logic
class CounterUp extends HTMLElement {
connectedCallback() { this.initObserver(); }
initObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => entry.isIntersecting && this.startCounters());
}, { threshold: 0.5 });
this.observer.observe(this);
}
startCounters() {
const count = this.querySelectorAll(".counter-item .heading");
count.forEach(c => {
const target = parseInt(c.getAttribute("data-target"), 10);
const speed = 100;
let current = 0;
const update = () => {
current += target / speed;
if (current < target) {
c.childNodes[0].textContent = Math.ceil(current);
requestAnimationFrame(update);
} else {
c.childNodes[0].textContent = target;
}
};
update();
});
}
}
customElements.define("counter-up", CounterUp);
Notable details
- Counter only fires when 50% of the strip is visible (
threshold: 0.5) — counting doesn't race ahead while the band is still below the fold. - The increment formula
target / 100means small numbers (25 years) and big numbers (500 clients) all settle in roughly the same number of frames — the speed feels uniform. c.childNodes[0]updates only the text node, leaving the<span>+</span>suffix intact.- The whole section's dark theme is achieved by overriding four
--color-*custom properties on the.promotionroot — no.darkmodifier classes anywhere.
Use when
- B2B / SaaS pages where the hero needs a "trust strip" with animated stats.
- Sections that should switch to dark mode contextually without a global theme toggle.
- Counter behavior that must wait until the user reads the section (rather than autoplaying as soon as the page loads).
Caveats
- The counter restarts on every section re-entry only if you remove
this.observer.disconnect()— current code keeps the observer alive but the function will start counters from 0 again on each crossing. Addoncelogic if undesired. - Negative or decimal
data-targetvalues aren't handled —parseIntwill silently round.