採用サイトでよく見かけるようになった、スクロールで数値がカウントアップ表示されるアレ。
- プラグインを利用したくない
- HTMLのdata属性でオプションを設定したい
というわけで、自作してみました!
仕様
全般
- 文書構造を考慮し、HTMLにはカウント後の数値を設定する
- カウントしたい要素にCSSで
opacity: 0;
を設定する- JavaScript でカウント後からカウント開始数へと変更しているため読み込み時のチラツキ対策
- カウントしたい要素にclass(
js-counter
)を設定する - 画面と数値が交差した時にclass(
is-inview
)が付与されたらカウント開始
オプション
カウントのオプションはHTMLのdata属性に設定します。
data属性 | デフォルト | 設定内容 |
---|---|---|
data-count-start | 0 | カウント開始数 |
data-count-duration | 2000 | アニメーション秒数(ミリ秒) |
data-count-easing | なし | イージング名 |
イージングについては止まる時に適用したいので、以下の4つが設定可能です。
- easeOutQuad
- easeOutCubic
- easeOutQuart
- easeOutQuint
JavaScript
Counter
JS
class Counter {
#defaults
#options
#easings
#counterList
constructor(config) {
this.#defaults = {
selector: '.js-counter',
duration: 2000,
easing: null,
inviewClass: 'is-inview'
}
this.#options = { ...this.#defaults, ...config }
this.#easings = {
easeOutQuad: (t) => t * (2 - t),
easeOutCubic: (t) => --t * t * t + 1,
easeOutQuart: (t) => 1 - --t * t * t * t,
easeOutQuint: (t) => 1 + --t * t * t * t * t
}
this.#counterList = document.querySelectorAll(this.#options.selector)
this.#init()
}
#init() {
for (const el of this.#counterList) {
this.#setStartNumber(el)
this.#observeNumber(el)
}
}
// カウント開始数を設定
#setStartNumber(el) {
const start = Number(el.dataset.countStart) || 0
el.dataset.countEnd = el.textContent
el.textContent = start
el.style.opacity = 1
}
// 数値のclass属性を監視
#observeNumber(el) {
// オブザーバーの作成
const observer = new MutationObserver(() => {
// 画面と数値が交差した時のclassが付与されたら
if (el.classList.contains(this.#options.inviewClass)) {
this.countNumber(el)
}
})
// 監視の開始
observer.observe(el, {
attributes: true,
attributeFilter: ['class']
})
}
// 数値のカウント
countNumber(el) {
const start = Number(el.dataset.countStart) || 0
const endStr = el.dataset.countEnd
const end = parseFloat(endStr)
const duration = Number(el.dataset.countDuration) || this.#options.duration
const decimal = endStr.split('.')[1]
const digits = typeof decimal === 'undefined' ? 0 : decimal.length
const easing = el.dataset.countEasing
? this.#easings[el.dataset.countEasing]
: this.#easings[this.#options.easing]
let startTime = null
// ブラウザのフレームを更新するタイミング(16ms)毎に実行する関数
const countAnimation = (currentTime) => {
if (!startTime) startTime = currentTime
let progress = Math.min((currentTime - startTime) / duration, 1)
// イージングの設定
if (easing) {
progress = easing(progress)
}
// htmlに数値を設定
el.textContent = digits
? (progress * (end - start) + start).toFixed(digits)
: Math.floor(progress * (end - start) + start)
// カウントアニメーションを終了
if (progress < 1) {
requestAnimationFrame(countAnimation)
}
}
requestAnimationFrame(countAnimation)
}
}
Inview
JS
class Inview {
#defaults
#options
#inviewList
constructor(config) {
this.#defaults = {
selector: '.js-inview',
inviewClass: 'is-inview'
}
this.#options = { ...this.#defaults, ...config }
this.#inviewList = document.querySelectorAll(this.#options.selector)
this.#init()
}
#init() {
for (const el of this.#inviewList) {
const options = {
rootMargin: el.dataset.inviewRootMargin || '0px'
}
const callback = (entries) => {
for (const entry of entries) {
const isInviewToggle = el.dataset.inviewToggle === ''
// 画面と要素が交差
if (entry.isIntersecting) {
el.classList.add(this.#options.inviewClass)
// target.dataset.inviewToggle属性がない場合
// 表示後は監視を解除
if (!isInviewToggle) {
observer.unobserve(el)
}
}
// data-inview-toggle属性がある場合
// 画面内から消えたら要素のclassを削除
if (!entry.isIntersecting && isInviewToggle) {
el.classList.remove(this.#options.inviewClass)
}
}
}
const observer = new IntersectionObserver(callback, options)
observer.observe(el)
}
}
}
DEMO
See the Pen Number Count Up Down by Takuya Mori (@taqumo) on CodePen.