Skip to content

Pagination

Guidelines

The Pagination component enables users to select a specific page from a range of pages, helping them navigate large sets of content in a clear and manageable way.

Main Component


Code

      nav.pagination ul.pagination-items {
        display: flex;
        flex-direction: row;
        justify-content: center;
        padding: 0;
        list-style: none;
        gap: 0.5rem;
        margin: 0;
      }

      .pagination .pagination-items .pagebutton {
        width: var(--token-pagination-button-size);
        height: var(--token-pagination-button-size);
        margin: 0;

        border-radius: 50%;
        display: flex;
        justify-content: center;
        align-items: center;

        background: none;
        border: none;
        cursor: pointer;

        transition: 0.3s;

        /* pageicon can be <a> or <button> depending on JS */
        .pageicon {
          display: flex;
          align-items: center;
          justify-content: center;

          color: var(--token-pagination-icon-default);

          /* if it is a button, reset default button styles */
          background: transparent;
          border: 0;
          padding: 0;
          font: inherit;
          line-height: inherit;
          cursor: inherit;
        }

        &:hover,
        &.hover {
          background-color: var(--token-pagination-bg-hover);
        }

        /* focus is usually on inner button, so use focus-within */
        &:focus-within,
        &.focused {
          background-color: var(--token-pagination-bg-focus);
        }

        &:active,
        &.selected {
          background-color: var(--token-pagination-bg-selected);

          .pageicon {
            color: var(--token-pagination-icon-active);
          }
        }

        &:disabled,
        &.disabled {
          cursor: not-allowed;

          .pageicon {
            color: var(--token-pagination-icon-disabled);
          }
        }
      }

      .pagination-items .pagebutton.-ctrl {
        background: none;
        border: var(--token-pagination-border-default);

        .pageicon .twemoji {
          height: var(--token-pagination-icon-size);
          width: var(--token-pagination-icon-size);
        }

        &:hover,
        &.hover {
          border: 0;
          background-color: var(--token-pagination-bg-hover);
        }

        &:disabled,
        &.disabled {
          border: var(--token-pagination-border-disabled);
          background: none;
          cursor: not-allowed;

          .pageicon {
            color: var(--token-pagination-icon-disabled);
          }
        }
      }

      .pagination-items .pagebutton.-more {
        cursor: default;
        pointer-events: none;

        .pageicon .twemoji {
          width: var(--token-pagination-icon-size);
          height: var(--token-pagination-icon-size);
        }
      }
    <div class="pagination">
        <ul class="pagination-items">
            <li class="pagebutton -ctrl"><a class="pageicon">:material-chevron-left:</a></li>
            <li class="pagebutton selected"><a class="pageicon">1</a></li>
            <li class="pagebutton"><a class="pageicon">2</a></li>
            <li class="pagebutton -more"><a class="pageicon">:material-dots-horizontal:</a></li>
            <li class="pagebutton"><a class="pageicon">5</a></li>
            <li class="pagebutton -ctrl"><a class="pageicon">:material-chevron-right:</a></li>
        </ul>
    </div>
    document.addEventListener("DOMContentLoaded", () => {
      const pagination = document.querySelector(".pagination");
      if (!pagination) return;

      const list = pagination.querySelector(".pagination-items");
      if (!list) return;

      // total pages: read from the largest number already in the HTML (e.g., "10")
      const nums = Array.from(
        list.querySelectorAll(".pagebutton:not(.-ctrl):not(.-more) .pageicon")
      )
        .map((el) => parseInt(el.textContent, 10))
        .filter(Number.isFinite);

      const totalPages = nums.length ? Math.max(...nums) : 10;

      // current page: read from .selected if present, else 1
      const selected = list.querySelector(".pagebutton.selected .pageicon");
      let currentPage = selected ? parseInt(selected.textContent, 10) : 1;
      if (!Number.isFinite(currentPage)) currentPage = 1;


      const prevIcon =
        list.querySelector(".pagebutton.-ctrl:first-child .pageicon")?.innerHTML || "‹";
      const nextIcon =
        list.querySelector(".pagebutton.-ctrl:last-child .pageicon")?.innerHTML || "›";

      function clamp(n, min, max) {
        return Math.max(min, Math.min(max, n));
      }

      function liButton(className, html, data = {}) {
        const li = document.createElement("li");
        li.className = className;

        const btn = document.createElement("button");
        btn.type = "button";
        btn.className = "pageicon";
        btn.innerHTML = html;

        Object.entries(data).forEach(([k, v]) => {
          li.dataset[k] = String(v);
        });

        li.appendChild(btn);
        return li;
      }

      function render() {
        list.innerHTML = "";

        // Prev
        const prev = liButton("pagebutton -ctrl", prevIcon, { role: "prev" });
        if (currentPage === 1) prev.classList.add("disabled");
        list.appendChild(prev);

        // Window behavior you requested:
        // - Start: 1 2 3 4 … last
        // - Sliding: current current+1 current+2 current+3 … last
        // - End: … last-4 last-3 last-2 last-1 last
        if (totalPages <= 5) {
          for (let p = 1; p <= totalPages; p++) {
            const item = liButton("pagebutton", String(p), { page: p });
            if (p === currentPage) item.classList.add("selected");
            list.appendChild(item);
          }
        } else {
          if (currentPage <= 1) currentPage = 1;

          // End case: show "... 6 7 8 9 10" style
          if (currentPage >= totalPages - 4) {
            list.appendChild(liButton("pagebutton -more", "…", { role: "more" }));

            for (let p = totalPages - 4; p <= totalPages; p++) {
              const item = liButton("pagebutton", String(p), { page: p });
              if (p === currentPage) item.classList.add("selected");
              list.appendChild(item);
            }
          } else {
            // Normal case: show sliding 4-number window + "... last"
            const start = clamp(currentPage, 1, totalPages - 4);
            for (let p = start; p <= start + 3; p++) {
              const item = liButton("pagebutton", String(p), { page: p });
              if (p === currentPage) item.classList.add("selected");
              list.appendChild(item);
            }

            list.appendChild(liButton("pagebutton -more", "…", { role: "more" }));

            const last = liButton("pagebutton", String(totalPages), { page: totalPages });
            if (currentPage === totalPages) last.classList.add("selected");
            list.appendChild(last);
          }
        }

        // Next
        const next = liButton("pagebutton -ctrl", nextIcon, { role: "next" });
        if (currentPage === totalPages) next.classList.add("disabled");
        list.appendChild(next);
      }

      list.addEventListener("click", (e) => {
        const li = e.target.closest(".pagebutton");
        if (!li) return;

        if (li.classList.contains("disabled")) return;
        if (li.classList.contains("-more")) return;

        const role = li.dataset.role;
        const page = li.dataset.page ? parseInt(li.dataset.page, 10) : NaN;

        if (role === "prev") currentPage = clamp(currentPage - 1, 1, totalPages);
        else if (role === "next") currentPage = clamp(currentPage + 1, 1, totalPages);
        else if (Number.isFinite(page)) currentPage = clamp(page, 1, totalPages);

        render();
      });

      render();
    });