VNL Works
VNL Works
VNL Works
← Components
Download ZIPRegistry JSON
Package: tailwind-motion-kit
patterns

Count Up

実績/統計の数値をスクロール時にカウントアップ。

numbersstatsjs
Customize

data-mk-to / data-mk-from / data-mk-duration など。

A11y

装飾なら aria-hidden。意味があるなら周辺文脈で補強。

Install

“壊れにくさ”優先のため、依存は増やさずに ファイルをコピーして使う 形式です。

Files

まずは下記ファイルをプロジェクトに配置してください。

required-files.md
- tailwind-motion-kit/js/count-up.js (js)
Option A

まずは Bundle を読み込んで、動作を確認するのがおすすめです。

globals.css
/* Bundle: まずはこれが一番簡単 */
@import "./tailwind-motion-kit/css/99_bundle.css";
Option B

必要な CSS だけ読み込みたい場合はこちら。

globals.css
/* このコンポーネントは CSS ファイルを持ちません */
JavaScript

動きやUI制御が必要な場合のみ、初期化を追加します。

init.js
import { initCountUp } from "./tailwind-motion-kit/js/count-up.js";
initCountUp();

Snippets

Copy & paste
usage.html
<strong data-mk-countup data-mk-to="12800" data-mk-duration="1200"></strong>
usage.js
import { initCountUp } from "./tailwind-motion-kit/js/count-up.js";
initCountUp();

Files

この部品が参照する実ファイルです。

tailwind-motion-kit/js/count-up.js
js
tailwind-motion-kit/js/count-up.js
// Tailwind Motion Kit (mk) - Count up
// ----------------------------------
// data-mk-countup 要素の数値を、inview でカウントアップさせます。
// Nyano の統計などに便利です。
// 例:
//   <span data-mk-countup data-mk-to="1234" data-mk-duration="1200"></span>

function prefersReducedMotion() {
  return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

function toMs(value, fallbackMs) {
  if (value == null || value === "") return fallbackMs;
  const v = String(value).trim();
  if (v.endsWith("ms")) return Number(v.replace("ms", "")) || fallbackMs;
  if (v.endsWith("s")) return (Number(v.replace("s", "")) * 1000) || fallbackMs;
  const n = Number(v);
  return Number.isFinite(n) ? n : fallbackMs;
}

function formatNumber(n, { decimals = 0, sep = "," } = {}) {
  const fixed = n.toFixed(decimals);
  const [a, b] = fixed.split(".");
  const withSep = a.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
  return b ? `${withSep}.${b}` : withSep;
}

export function initCountUp({
  selector = "[data-mk-countup]",
  threshold = 0.2,
  rootMargin = "0px 0px -10% 0px",
  once = true,
} = {}) {
  const reduce = prefersReducedMotion();
  const els = Array.from(document.querySelectorAll(selector));

  const play = (el) => {
    if (el.getAttribute("data-mk-countup-played") === "true") return;

    const to = Number(el.getAttribute("data-mk-to") ?? el.textContent ?? 0);
    const from = Number(el.getAttribute("data-mk-from") ?? 0);

    const duration = toMs(el.getAttribute("data-mk-duration"), 1200);
    const decimals = Number(el.getAttribute("data-mk-decimals") ?? 0);
    const sep = el.getAttribute("data-mk-sep") ?? ",";

    const prefix = el.getAttribute("data-mk-prefix") ?? "";
    const suffix = el.getAttribute("data-mk-suffix") ?? "";

    if (reduce || duration <= 0) {
      el.textContent = `${prefix}${formatNumber(to, { decimals, sep })}${suffix}`;
      el.setAttribute("data-mk-countup-played", "true");
      return;
    }

    const start = performance.now();
    const delta = to - from;

    const tick = (now) => {
      const t = Math.min(1, (now - start) / duration);
      // ease-out
      const eased = 1 - Math.pow(1 - t, 3);
      const value = from + delta * eased;

      el.textContent = `${prefix}${formatNumber(value, { decimals, sep })}${suffix}`;

      if (t < 1) {
        requestAnimationFrame(tick);
      } else {
        el.textContent = `${prefix}${formatNumber(to, { decimals, sep })}${suffix}`;
        el.setAttribute("data-mk-countup-played", "true");
      }
    };

    requestAnimationFrame(tick);
  };

  if (reduce) {
    els.forEach(play);
    return { destroy() {}, refresh() { els.forEach(play); } };
  }

  const io = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;
      play(entry.target);
      if (once) io.unobserve(entry.target);
    });
  }, { threshold, rootMargin });

  els.forEach((el) => io.observe(el));

  return {
    destroy() { io.disconnect(); },
    refresh() {
      document.querySelectorAll(selector).forEach((el) => io.observe(el));
    },
  };
}
Preview
Count up (static preview)
12,800+

実運用では initCountUp() でスクロール連動のカウントアップが可能です。

Tips
  • • デザイン側は CSS 変数(tokens)で一括調整できます。
  • • 動きは “短く・小さく・同じ癖” を優先すると品位が保てます。
  • • prefers-reduced-motion を必ず考慮してください。