ページネーション

数字とページ送りボタンで構成するページネーション。現在地から離れた番号は「…」に省略され、クリックすると即座にページが切り替わります。

#css#js#nav#pagination

ライブデモ

コード

HTML
<!-- 数字+前後+省略(…)のページネーション。現在ページを強調し、クリックで切り替える -->
<div class="npg-frame">
  <div class="npg-stage">
    <p class="npg-eyebrow">Kioku Journal — 全89件の記事</p>
    <p class="npg-current" id="npgCurrentLabel">3 / 9 ページ</p>
    <nav class="npg-nav" aria-label="ページネーション" id="npgNav">
      <button class="npg-arrow" type="button" data-nav="prev" aria-label="前のページへ">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 6l-6 6 6 6"></path></svg>
      </button>
      <ul class="npg-list" id="npgList"></ul>
      <button class="npg-arrow" type="button" data-nav="next" aria-label="次のページへ">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"></path></svg>
      </button>
    </nav>
  </div>
</div>
CSS
* { box-sizing: border-box; }
body { margin: 0; display: block; background: #f8f8fb; font-family: "Hiragino Sans", "Segoe UI", system-ui, sans-serif; }

/* ルート: width:100% + height:100vh。中身は flex の margin:auto で常に中央に収める */
.npg-frame { position: relative; width: 100%; height: 100vh; overflow: hidden; background: #f8f8fb; display: flex; }
.npg-stage { margin: auto; text-align: center; padding: 24px; }

.npg-eyebrow { margin: 0 0 4px; font-size: 12px; font-weight: 700; color: #8b91a5; letter-spacing: .02em; }
.npg-current { margin: 0 0 18px; font-size: 20px; font-weight: 800; color: #1c2033; }

.npg-nav { display: inline-flex; align-items: center; gap: 6px; }
.npg-arrow {
  width: 34px; height: 34px; border-radius: 9px; border: 1px solid #e2e4ee;
  background: #fff; color: #4b4f63; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .18s ease, border-color .18s ease, opacity .18s ease;
}
.npg-arrow svg { width: 15px; height: 15px; }
.npg-arrow svg path { fill: none; stroke: currentColor; stroke-width: 2.3; stroke-linecap: round; stroke-linejoin: round; }
.npg-arrow:hover:not(:disabled) { background: #eef0fb; border-color: #cfd3ec; }
.npg-arrow:disabled { opacity: .35; cursor: not-allowed; }

.npg-list { list-style: none; display: flex; align-items: center; gap: 4px; margin: 0; padding: 0; }
.npg-num {
  min-width: 34px; height: 34px; padding: 0 6px; border-radius: 9px; border: 1px solid transparent;
  background: none; color: #4b4f63; font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer;
  transition: background .18s ease, color .18s ease, transform .15s ease;
}
.npg-num:hover { background: #eef0fb; color: #4338ca; }
.npg-num.is-current { background: #4338ca; color: #fff; font-weight: 800; transform: translateY(-1px); }
.npg-ellipsis { min-width: 20px; text-align: center; color: #b3b7c8; font-size: 13px; user-select: none; }

@media (max-width: 420px) {
  .npg-num { min-width: 30px; height: 30px; font-size: 12.5px; }
  .npg-arrow { width: 30px; height: 30px; }
}

@media (prefers-reduced-motion: reduce) {
  .npg-num { transition: none; }
}
JavaScript
// クリックでページを切替。現在地から離れたページ番号は「…」に省略する(null安全)
(() => {
  const nav = document.getElementById('npgNav');
  const list = document.getElementById('npgList');
  const label = document.getElementById('npgCurrentLabel');
  if (!nav || !list) return;

  const TOTAL = 9;
  let current = 3;

  const pagesToShow = () => {
    const set = new Set([1, TOTAL, current, current - 1, current + 1]);
    return [...set].filter((p) => p >= 1 && p <= TOTAL).sort((a, b) => a - b);
  };

  const render = () => {
    const pages = pagesToShow();
    list.innerHTML = '';
    let prev = 0;
    pages.forEach((p) => {
      if (p - prev > 1) {
        const gap = document.createElement('li');
        gap.className = 'npg-ellipsis';
        gap.textContent = '…';
        gap.setAttribute('aria-hidden', 'true');
        list.appendChild(gap);
      }
      const li = document.createElement('li');
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'npg-num';
      btn.textContent = String(p);
      if (p === current) {
        btn.classList.add('is-current');
        btn.setAttribute('aria-current', 'page');
      }
      btn.addEventListener('click', () => { current = p; render(); });
      li.appendChild(btn);
      list.appendChild(li);
      prev = p;
    });
    if (label) label.textContent = `${current} / ${TOTAL} ページ`;
    nav.querySelectorAll('[data-nav]').forEach((b) => {
      const isPrev = b.dataset.nav === 'prev';
      b.disabled = isPrev ? current <= 1 : current >= TOTAL;
    });
  };

  nav.addEventListener('click', (e) => {
    const btn = e.target.closest('[data-nav]');
    if (!btn) return;
    if (btn.dataset.nav === 'prev' && current > 1) current -= 1;
    if (btn.dataset.nav === 'next' && current < TOTAL) current += 1;
    render();
  });

  render();
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ページネーション」の効果を追加してください。

# 追加してほしい効果
ページネーション(ヘッダー & ナビ)
数字とページ送りボタンで構成するページネーション。現在地から離れた番号は「…」に省略され、クリックすると即座にページが切り替わります。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 数字+前後+省略(…)のページネーション。現在ページを強調し、クリックで切り替える -->
<div class="npg-frame">
  <div class="npg-stage">
    <p class="npg-eyebrow">Kioku Journal — 全89件の記事</p>
    <p class="npg-current" id="npgCurrentLabel">3 / 9 ページ</p>
    <nav class="npg-nav" aria-label="ページネーション" id="npgNav">
      <button class="npg-arrow" type="button" data-nav="prev" aria-label="前のページへ">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 6l-6 6 6 6"></path></svg>
      </button>
      <ul class="npg-list" id="npgList"></ul>
      <button class="npg-arrow" type="button" data-nav="next" aria-label="次のページへ">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"></path></svg>
      </button>
    </nav>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }
body { margin: 0; display: block; background: #f8f8fb; font-family: "Hiragino Sans", "Segoe UI", system-ui, sans-serif; }

/* ルート: width:100% + height:100vh。中身は flex の margin:auto で常に中央に収める */
.npg-frame { position: relative; width: 100%; height: 100vh; overflow: hidden; background: #f8f8fb; display: flex; }
.npg-stage { margin: auto; text-align: center; padding: 24px; }

.npg-eyebrow { margin: 0 0 4px; font-size: 12px; font-weight: 700; color: #8b91a5; letter-spacing: .02em; }
.npg-current { margin: 0 0 18px; font-size: 20px; font-weight: 800; color: #1c2033; }

.npg-nav { display: inline-flex; align-items: center; gap: 6px; }
.npg-arrow {
  width: 34px; height: 34px; border-radius: 9px; border: 1px solid #e2e4ee;
  background: #fff; color: #4b4f63; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .18s ease, border-color .18s ease, opacity .18s ease;
}
.npg-arrow svg { width: 15px; height: 15px; }
.npg-arrow svg path { fill: none; stroke: currentColor; stroke-width: 2.3; stroke-linecap: round; stroke-linejoin: round; }
.npg-arrow:hover:not(:disabled) { background: #eef0fb; border-color: #cfd3ec; }
.npg-arrow:disabled { opacity: .35; cursor: not-allowed; }

.npg-list { list-style: none; display: flex; align-items: center; gap: 4px; margin: 0; padding: 0; }
.npg-num {
  min-width: 34px; height: 34px; padding: 0 6px; border-radius: 9px; border: 1px solid transparent;
  background: none; color: #4b4f63; font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer;
  transition: background .18s ease, color .18s ease, transform .15s ease;
}
.npg-num:hover { background: #eef0fb; color: #4338ca; }
.npg-num.is-current { background: #4338ca; color: #fff; font-weight: 800; transform: translateY(-1px); }
.npg-ellipsis { min-width: 20px; text-align: center; color: #b3b7c8; font-size: 13px; user-select: none; }

@media (max-width: 420px) {
  .npg-num { min-width: 30px; height: 30px; font-size: 12.5px; }
  .npg-arrow { width: 30px; height: 30px; }
}

@media (prefers-reduced-motion: reduce) {
  .npg-num { transition: none; }
}

【JavaScript】
// クリックでページを切替。現在地から離れたページ番号は「…」に省略する(null安全)
(() => {
  const nav = document.getElementById('npgNav');
  const list = document.getElementById('npgList');
  const label = document.getElementById('npgCurrentLabel');
  if (!nav || !list) return;

  const TOTAL = 9;
  let current = 3;

  const pagesToShow = () => {
    const set = new Set([1, TOTAL, current, current - 1, current + 1]);
    return [...set].filter((p) => p >= 1 && p <= TOTAL).sort((a, b) => a - b);
  };

  const render = () => {
    const pages = pagesToShow();
    list.innerHTML = '';
    let prev = 0;
    pages.forEach((p) => {
      if (p - prev > 1) {
        const gap = document.createElement('li');
        gap.className = 'npg-ellipsis';
        gap.textContent = '…';
        gap.setAttribute('aria-hidden', 'true');
        list.appendChild(gap);
      }
      const li = document.createElement('li');
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'npg-num';
      btn.textContent = String(p);
      if (p === current) {
        btn.classList.add('is-current');
        btn.setAttribute('aria-current', 'page');
      }
      btn.addEventListener('click', () => { current = p; render(); });
      li.appendChild(btn);
      list.appendChild(li);
      prev = p;
    });
    if (label) label.textContent = `${current} / ${TOTAL} ページ`;
    nav.querySelectorAll('[data-nav]').forEach((b) => {
      const isPrev = b.dataset.nav === 'prev';
      b.disabled = isPrev ? current <= 1 : current >= TOTAL;
    });
  };

  nav.addEventListener('click', (e) => {
    const btn = e.target.closest('[data-nav]');
    if (!btn) return;
    if (btn.dataset.nav === 'prev' && current > 1) current -= 1;
    if (btn.dataset.nav === 'next' && current < TOTAL) current += 1;
    render();
  });

  render();
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。