09数字ページネーション
ページ番号ボタンが並び、ページ数が増えると中央が三点リーダーに折りたたまれ、現在のページだけが強調される。
使いどころ掲示板リストの下部、検索結果のページ移動
current page stateellipsis collapse 계산prev/next disabled

<main class="stage">
<nav class="pg-nav is-demo" aria-label="ページ">
<button class="pg-nav__prev" type="button" aria-label="前のページ">‹</button>
<ul class="pg-nav__list"></ul>
<button class="pg-nav__next" type="button" aria-label="次のページ">›</button>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-instant; // 버튼 강조가 바뀌는 시간
$easing: $ease-out;
$color: $subject-blue; // 현재 페이지 강조 색
@include stage($stage-ink);
.pg-nav {
display: flex;
align-items: center;
gap: $sp-1;
}
.pg-nav__list {
display: flex;
align-items: center;
gap: $sp-1;
list-style: none;
margin: 0;
padding: 0;
}
.pg-nav__prev, .pg-nav__next, .pg-nav__num {
min-width: 28px;
height: 28px;
padding: 0 $sp-1;
border: 0;
border-radius: $r-pill;
background: $stage-paper;
color: $stage-ink;
font-size: 12px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: background-color $duration $easing, color $duration $easing;
}
.pg-nav__prev:disabled, .pg-nav__next:disabled { opacity: .35; cursor: default; }
.pg-nav__num[aria-current="page"] { background: $color; color: $stage-paper; }
.pg-nav__num:focus-visible, .pg-nav__prev:focus-visible, .pg-nav__next:focus-visible {
outline: 2px solid $stage-yellow; outline-offset: 2px;
}
.pg-nav__ellipsis { color: rgba($stage-paper, .6); font-size: 12px; padding: 0 $sp-1; }
@media (prefers-reduced-motion: reduce) {
.pg-nav__prev, .pg-nav__next, .pg-nav__num { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pg-nav {
display: flex;
align-items: center;
gap: 4px;
}
.pg-nav__list {
display: flex;
align-items: center;
gap: 4px;
list-style: none;
margin: 0;
padding: 0;
}
.pg-nav__prev, .pg-nav__next, .pg-nav__num {
min-width: 28px;
height: 28px;
padding: 0 4px;
border: 0;
border-radius: 999px;
background: #ffffff;
color: #17141a;
font-size: 12px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: background-color 140ms ease-out, color 140ms ease-out;
}
.pg-nav__prev:disabled, .pg-nav__next:disabled {
opacity: 0.35;
cursor: default;
}
.pg-nav__num[aria-current=page] {
background: #2f6df6;
color: #ffffff;
}
.pg-nav__num:focus-visible, .pg-nav__prev:focus-visible, .pg-nav__next:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: 2px;
}
.pg-nav__ellipsis {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
padding: 0 4px;
}
@media (prefers-reduced-motion: reduce) {
.pg-nav__prev, .pg-nav__next, .pg-nav__num {
transition: none;
}
}(function () {
var nav = document.querySelector('.pg-nav');
var list = nav.querySelector('.pg-nav__list');
var prev = nav.querySelector('.pg-nav__prev');
var next = nav.querySelector('.pg-nav__next');
var total = 8, current = 3, timer = null;
function pages() {
var out = [];
for (var p = 1; p <= total; p++) {
if (p === 1 || p === total || Math.abs(p - current) <= 1) out.push(p);
else if (out[out.length - 1] !== '...') out.push('...');
}
return out;
}
function render() {
list.innerHTML = '';
pages().forEach(function (p) {
var li = document.createElement('li');
if (p === '...') {
li.className = 'pg-nav__ellipsis'; li.textContent = '...'; li.setAttribute('aria-hidden', 'true');
} else {
var b = document.createElement('button');
b.type = 'button'; b.className = 'pg-nav__num'; b.textContent = String(p);
if (p === current) { b.setAttribute('aria-current', 'page'); }
b.addEventListener('click', function () { stop(); current = p; render(); });
li.appendChild(b);
}
list.appendChild(li);
});
prev.disabled = current === 1;
next.disabled = current === total;
}
function stop() { nav.classList.remove('is-demo'); if (timer) { clearInterval(timer); timer = null; } }
prev.addEventListener('click', function () { stop(); if (current > 1) { current--; render(); } });
next.addEventListener('click', function () { stop(); if (current < total) { current++; render(); } });
render();
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
timer = setInterval(function () {
current = current < total ? current + 1 : 1;
render();
}, 260);
}
})();import { useState, type CSSProperties } from "react";
import styles from "./NumberedPagination.module.scss";
export type NumberedPaginationProps = {
total: number;
initial?: number;
/** 현재 페이지 강조 색 */
color?: string;
};
type Vars = CSSProperties & { "--color"?: string };
type Token = number | "...";
function pages(total: number, current: number): Token[] {
const out: Token[] = [];
for (let p = 1; p <= total; p++) {
if (p === 1 || p === total || Math.abs(p - current) <= 1) out.push(p);
else if (out[out.length - 1] !== "...") out.push("...");
}
return out;
}
export default function NumberedPagination({ total, initial = 1, color = "#2f6df6" }: NumberedPaginationProps) {
const [current, setCurrent] = useState(initial);
const vars: Vars = { "--color": color };
return (
<nav className={styles.nav} style={vars} aria-label="페이지">
<button
className={styles.prev}
type="button"
aria-label="이전 페이지"
disabled={current === 1}
onClick={() => setCurrent((c) => Math.max(1, c - 1))}
>‹</button>
<ul className={styles.list}>
{pages(total, current).map((p, i) =>
p === "..." ? (
<li className={styles.ellipsis} key={`e${i}`} aria-hidden="true">...</li>
) : (
<li key={p}>
<button
className={styles.num}
type="button"
aria-current={p === current ? "page" : undefined}
onClick={() => setCurrent(p)}
>{p}</button>
</li>
)
)}
</ul>
<button
className={styles.next}
type="button"
aria-label="다음 페이지"
disabled={current === total}
onClick={() => setCurrent((c) => Math.min(total, c + 1))}
>›</button>
</nav>
);
}09 数字ページネーション — 計測 累積面積 4.884% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · スワイプ削除リストUI 9選 — ページネーションまで
このデモが属する記事
スワイプ削除リストUI 9選 — ページネーションまでスワイプ削除とは、リストの行を横に押すと裏に隠れていた削除ボタンが現れる動きのことで、アプリで見たあの操作をウェブでもそのまま使いたい人は多いです。この記事では、そのスワイプ削除を含むリストのインタラクションを9個、実際に動くコードでまとめ この記事のデモをすべて見る


