Pagination
긴 목록을 페이지로 나누어 이동합니다. 총 건수와 페이지 크기 선택을 함께 둡니다.
1. Default
이전/다음 + 번호. 현재 .on + aria-current. 첫 페이지에서는 이전 비활성.
HTML
<nav class="pagination" aria-label="페이지">
<button type="button" aria-label="이전" disabled><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6l6 6"/></svg></button>
<button type="button" class="on" aria-current="page">1</button>
<button type="button">2</button>
<button type="button">3</button>
<button type="button">4</button>
<span class="gap">…</span>
<button type="button">24</button>
<button type="button" aria-label="다음"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6l-6 6"/></svg></button>
</nav>CSS · .pagination, .pagination-bar, .dropdown, .menu, .menu-item
/* JS Design System v0.6 · .pagination, .pagination-bar, .dropdown, .menu, .menu-item · 토큰(var(--*))은 assets/style.css :root 참고 */
.dropdown{position:relative;display:inline-block}
.dropdown .trigger{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 12px 0 14px;border:1px solid var(--border-strong);border-radius:var(--radius-md);background:var(--bg-surface);font:inherit;font-size:14px;color:var(--text-primary);cursor:pointer;min-width:200px;justify-content:space-between}
.dropdown .trigger:hover{border-color:var(--border-strong)}
.dropdown .trigger .placeholder{color:var(--text-disabled)}
.dropdown .trigger::after{content:"";width:16px;height:16px;flex:none;background:var(--text-secondary);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;transition:transform var(--motion-fast)}
.dropdown.open .trigger{border-color:var(--border-focus)}
.dropdown.open .trigger::after{transform:rotate(180deg)}
.dropdown .trigger[disabled]{opacity:.4;cursor:not-allowed}
.dropdown .menu{position:absolute;top:calc(100% + 4px);left:0;display:none;z-index:5;min-width:100%}
.dropdown.open .menu{display:block}
.dropdown.up .menu{top:auto;bottom:calc(100% + 4px)}
.menu{min-width:200px;padding:6px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);box-shadow:var(--shadow-2);list-style:none;margin:0;max-height:min(320px,60vh);overflow-y:auto;overscroll-behavior:contain}
.menu-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:6px;font-size:14px;cursor:pointer;color:var(--text-primary);white-space:nowrap}
.menu-item:hover{background:var(--bg-subtle)}
.menu-item.on{background:var(--accent-subtle);color:var(--accent-text);font-weight:600}
.menu-item.danger{color:var(--danger-text)}
.menu-item[aria-disabled="true"]{opacity:.4;cursor:not-allowed;pointer-events:none}
.menu-item .hint{margin-left:auto;font-size:12px;color:var(--text-tertiary)}
.menu-item svg{width:16px;height:16px;flex:none;color:var(--text-tertiary)}
.pagination{display:inline-flex;align-items:center;gap:4px}
.pagination button{min-width:36px;height:36px;padding:0 8px;border:1px solid transparent;border-radius:var(--radius-md);background:transparent;font:inherit;font-size:14px;color:var(--text-secondary);cursor:pointer}
.pagination button:hover{background:var(--bg-subtle);color:var(--text-primary)}
.pagination button.on{background:var(--accent);color:var(--accent-on);font-weight:600}
.pagination button[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.pagination button svg{width:16px;height:16px;display:block}
.pagination .gap{min-width:36px;text-align:center;color:var(--text-disabled)}
.pagination.compact button{min-width:32px;height:32px;font-size:12px}
.pagination.outline button{border-color:var(--border-default);background:var(--bg-surface)}
.pagination.outline button.on{border-color:var(--accent); color:var(--accent-text);}
.pagination-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;font-size:12px;color:var(--text-secondary)}
.pagination-bar .dropdown .trigger{height:32px;min-width:120px;font-size:12px}React · @jiran/ds-react
import { useState } from "react";
import { Pagination } from "@jiran/ds-react";
export default function Example() {
const [page, setPage] = useState(1);
return <Pagination page={page} total={24} siblings={3} onChange={setPage} />;
}2. Compact / Outline
HTML
<nav class="pagination compact" aria-label="페이지">
<button type="button" aria-label="이전"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6l6 6"/></svg></button><button type="button">7</button><button type="button" class="on" aria-current="page">8</button><button type="button">9</button><button type="button" aria-label="다음"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6l-6 6"/></svg></button>
</nav>
<nav class="pagination outline" aria-label="페이지">
<button type="button" aria-label="이전" disabled><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6l6 6"/></svg></button><button type="button" class="on" aria-current="page">1</button><button type="button">2</button><button type="button">3</button><button type="button" aria-label="다음"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6l-6 6"/></svg></button>
</nav>CSS · .pagination, .pagination-bar, .dropdown, .menu, .menu-item
/* JS Design System v0.6 · .pagination, .pagination-bar, .dropdown, .menu, .menu-item · 토큰(var(--*))은 assets/style.css :root 참고 */
.dropdown{position:relative;display:inline-block}
.dropdown .trigger{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 12px 0 14px;border:1px solid var(--border-strong);border-radius:var(--radius-md);background:var(--bg-surface);font:inherit;font-size:14px;color:var(--text-primary);cursor:pointer;min-width:200px;justify-content:space-between}
.dropdown .trigger:hover{border-color:var(--border-strong)}
.dropdown .trigger .placeholder{color:var(--text-disabled)}
.dropdown .trigger::after{content:"";width:16px;height:16px;flex:none;background:var(--text-secondary);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;transition:transform var(--motion-fast)}
.dropdown.open .trigger{border-color:var(--border-focus)}
.dropdown.open .trigger::after{transform:rotate(180deg)}
.dropdown .trigger[disabled]{opacity:.4;cursor:not-allowed}
.dropdown .menu{position:absolute;top:calc(100% + 4px);left:0;display:none;z-index:5;min-width:100%}
.dropdown.open .menu{display:block}
.dropdown.up .menu{top:auto;bottom:calc(100% + 4px)}
.menu{min-width:200px;padding:6px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);box-shadow:var(--shadow-2);list-style:none;margin:0;max-height:min(320px,60vh);overflow-y:auto;overscroll-behavior:contain}
.menu-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:6px;font-size:14px;cursor:pointer;color:var(--text-primary);white-space:nowrap}
.menu-item:hover{background:var(--bg-subtle)}
.menu-item.on{background:var(--accent-subtle);color:var(--accent-text);font-weight:600}
.menu-item.danger{color:var(--danger-text)}
.menu-item[aria-disabled="true"]{opacity:.4;cursor:not-allowed;pointer-events:none}
.menu-item .hint{margin-left:auto;font-size:12px;color:var(--text-tertiary)}
.menu-item svg{width:16px;height:16px;flex:none;color:var(--text-tertiary)}
.pagination{display:inline-flex;align-items:center;gap:4px}
.pagination button{min-width:36px;height:36px;padding:0 8px;border:1px solid transparent;border-radius:var(--radius-md);background:transparent;font:inherit;font-size:14px;color:var(--text-secondary);cursor:pointer}
.pagination button:hover{background:var(--bg-subtle);color:var(--text-primary)}
.pagination button.on{background:var(--accent);color:var(--accent-on);font-weight:600}
.pagination button[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.pagination button svg{width:16px;height:16px;display:block}
.pagination .gap{min-width:36px;text-align:center;color:var(--text-disabled)}
.pagination.compact button{min-width:32px;height:32px;font-size:12px}
.pagination.outline button{border-color:var(--border-default);background:var(--bg-surface)}
.pagination.outline button.on{border-color:var(--accent); color:var(--accent-text);}
.pagination-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;font-size:12px;color:var(--text-secondary)}
.pagination-bar .dropdown .trigger{height:32px;min-width:120px;font-size:12px}React · @jiran/ds-react
import { useState } from "react";
import { Pagination } from "@jiran/ds-react";
export default function Example() {
const [a, setA] = useState(8);
const [b, setB] = useState(1);
return (
<>
<Pagination compact boundaries={0} page={a} total={20} onChange={setA} />
<Pagination outline page={b} total={3} onChange={setB} />
</>
);
}3. Pagination Bar
테이블 하단. 총 건수 · 페이지 크기 · 페이지 이동.
HTML
<div class="pagination-bar">
<span>총 <b>1,284</b>건 · 1–50</span>
<div style="display:flex;align-items:center;gap:12px">
<div class="dropdown"><button type="button" class="trigger" aria-haspopup="listbox" aria-expanded="false">50개씩</button></div>
<nav class="pagination compact" aria-label="페이지"><button type="button" aria-label="이전" disabled><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6l6 6"/></svg></button><button type="button" class="on" aria-current="page">1</button><button type="button">2</button><button type="button">3</button><span class="gap">…</span><button type="button">26</button><button type="button" aria-label="다음"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6l-6 6"/></svg></button></nav>
</div>
</div>CSS · .pagination, .pagination-bar, .dropdown, .menu, .menu-item
/* JS Design System v0.6 · .pagination, .pagination-bar, .dropdown, .menu, .menu-item · 토큰(var(--*))은 assets/style.css :root 참고 */
.dropdown{position:relative;display:inline-block}
.dropdown .trigger{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 12px 0 14px;border:1px solid var(--border-strong);border-radius:var(--radius-md);background:var(--bg-surface);font:inherit;font-size:14px;color:var(--text-primary);cursor:pointer;min-width:200px;justify-content:space-between}
.dropdown .trigger:hover{border-color:var(--border-strong)}
.dropdown .trigger .placeholder{color:var(--text-disabled)}
.dropdown .trigger::after{content:"";width:16px;height:16px;flex:none;background:var(--text-secondary);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;transition:transform var(--motion-fast)}
.dropdown.open .trigger{border-color:var(--border-focus)}
.dropdown.open .trigger::after{transform:rotate(180deg)}
.dropdown .trigger[disabled]{opacity:.4;cursor:not-allowed}
.dropdown .menu{position:absolute;top:calc(100% + 4px);left:0;display:none;z-index:5;min-width:100%}
.dropdown.open .menu{display:block}
.dropdown.up .menu{top:auto;bottom:calc(100% + 4px)}
.menu{min-width:200px;padding:6px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);box-shadow:var(--shadow-2);list-style:none;margin:0;max-height:min(320px,60vh);overflow-y:auto;overscroll-behavior:contain}
.menu-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:6px;font-size:14px;cursor:pointer;color:var(--text-primary);white-space:nowrap}
.menu-item:hover{background:var(--bg-subtle)}
.menu-item.on{background:var(--accent-subtle);color:var(--accent-text);font-weight:600}
.menu-item.danger{color:var(--danger-text)}
.menu-item[aria-disabled="true"]{opacity:.4;cursor:not-allowed;pointer-events:none}
.menu-item .hint{margin-left:auto;font-size:12px;color:var(--text-tertiary)}
.menu-item svg{width:16px;height:16px;flex:none;color:var(--text-tertiary)}
.pagination{display:inline-flex;align-items:center;gap:4px}
.pagination button{min-width:36px;height:36px;padding:0 8px;border:1px solid transparent;border-radius:var(--radius-md);background:transparent;font:inherit;font-size:14px;color:var(--text-secondary);cursor:pointer}
.pagination button:hover{background:var(--bg-subtle);color:var(--text-primary)}
.pagination button.on{background:var(--accent);color:var(--accent-on);font-weight:600}
.pagination button[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.pagination button svg{width:16px;height:16px;display:block}
.pagination .gap{min-width:36px;text-align:center;color:var(--text-disabled)}
.pagination.compact button{min-width:32px;height:32px;font-size:12px}
.pagination.outline button{border-color:var(--border-default);background:var(--bg-surface)}
.pagination.outline button.on{border-color:var(--accent); color:var(--accent-text);}
.pagination-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;font-size:12px;color:var(--text-secondary)}
.pagination-bar .dropdown .trigger{height:32px;min-width:120px;font-size:12px}React · @jiran/ds-react
import { useState } from "react";
import { Pagination, PaginationBar, Select } from "@jiran/ds-react";
export default function Example() {
const [page, setPage] = useState(1);
const [size, setSize] = useState<string | null>("50");
return (
<PaginationBar summary={<>총 <b>1,284</b>건 · {(page - 1) * Number(size) + 1}–{Math.min(page * Number(size), 1284)}</>}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<Select options={[{ value: "50", label: "50개씩" }, { value: "100", label: "100개씩" }]} value={size} onChange={setSize} triggerStyle={{ height: 32, minWidth: 120, fontSize: 12 }} />
<Pagination compact page={page} total={Math.ceil(1284 / Number(size))} siblings={2} onChange={setPage} />
</div>
</PaginationBar>
);
}Props
HTML 에서는 클래스와 속성으로, React(@jiran/ds-react)에서는 같은 이름의 prop(variant · size …)으로 변형·상태를 지정합니다.
| Prop / Class | Type · Values | Default | Description |
|---|---|---|---|
| .pagination | nav | — | aria-label=페이지. 버튼 36 (compact 32) |
| button.on | current | — | 현재 페이지 + aria-current=page |
| .gap | span | — | 생략(…) |
| .compact / .outline | boolean | false | 작은 크기 / 보더형 |
| .pagination-bar | container | — | 총 건수 + 페이지 크기 + 페이지네이션 |
Usage
React: @jiran/ds-react 패키지를 설치하고 style.css 를 한 번 import 하면 위 React 탭 코드를 그대로 쓸 수 있습니다(React Package). HTML: assets/style.css 를 연결하고(일부만 필요하면 아래 컴포넌트 CSS 복사), 동작이 필요하면 assets/ds.js 를 함께 넣습니다 — 이 페이지의 프리뷰가 그 스크립트로 동작합니다. 색은 전부 토큰(var(--accent) 등)을 참조하므로 --accent-* 만 제품 컬러로 바꾸면 됩니다.
React @jiran/ds-react
npm i @jiran/ds-react
// main.tsx — 앱 진입점에서 한 번
import "@jiran/ds-react/style.css";
// 컴포넌트
import { Pagination, PaginationBar, paginate } from "@jiran/ds-react";HTML
<!-- 1) 토큰 + 전체 컴포넌트 스타일 한 번에 연결 -->
<link rel="stylesheet" href="assets/style.css">
<!-- 동작(드롭다운·탭·달력·표 정렬/선택·토스트 등) — 순수 JS 한 파일. React 제품은 불필요 -->
<script src="assets/ds.js" defer></script>
<!-- 2) 또는 아래 "Component CSS" 만 프로젝트 CSS 에 붙이고,
:root 토큰은 Foundations › Design Token 에서 복사 -->Component CSS .pagination, .pagination-bar, .dropdown, .menu, .menu-item
/* JS Design System v0.6 · .pagination, .pagination-bar, .dropdown, .menu, .menu-item · 토큰(var(--*))은 assets/style.css :root 참고 */
.dropdown{position:relative;display:inline-block}
.dropdown .trigger{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 12px 0 14px;border:1px solid var(--border-strong);border-radius:var(--radius-md);background:var(--bg-surface);font:inherit;font-size:14px;color:var(--text-primary);cursor:pointer;min-width:200px;justify-content:space-between}
.dropdown .trigger:hover{border-color:var(--border-strong)}
.dropdown .trigger .placeholder{color:var(--text-disabled)}
.dropdown .trigger::after{content:"";width:16px;height:16px;flex:none;background:var(--text-secondary);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E") center/contain no-repeat;transition:transform var(--motion-fast)}
.dropdown.open .trigger{border-color:var(--border-focus)}
.dropdown.open .trigger::after{transform:rotate(180deg)}
.dropdown .trigger[disabled]{opacity:.4;cursor:not-allowed}
.dropdown .menu{position:absolute;top:calc(100% + 4px);left:0;display:none;z-index:5;min-width:100%}
.dropdown.open .menu{display:block}
.dropdown.up .menu{top:auto;bottom:calc(100% + 4px)}
.menu{min-width:200px;padding:6px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);box-shadow:var(--shadow-2);list-style:none;margin:0;max-height:min(320px,60vh);overflow-y:auto;overscroll-behavior:contain}
.menu-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:6px;font-size:14px;cursor:pointer;color:var(--text-primary);white-space:nowrap}
.menu-item:hover{background:var(--bg-subtle)}
.menu-item.on{background:var(--accent-subtle);color:var(--accent-text);font-weight:600}
.menu-item.danger{color:var(--danger-text)}
.menu-item[aria-disabled="true"]{opacity:.4;cursor:not-allowed;pointer-events:none}
.menu-item .hint{margin-left:auto;font-size:12px;color:var(--text-tertiary)}
.menu-item svg{width:16px;height:16px;flex:none;color:var(--text-tertiary)}
.pagination{display:inline-flex;align-items:center;gap:4px}
.pagination button{min-width:36px;height:36px;padding:0 8px;border:1px solid transparent;border-radius:var(--radius-md);background:transparent;font:inherit;font-size:14px;color:var(--text-secondary);cursor:pointer}
.pagination button:hover{background:var(--bg-subtle);color:var(--text-primary)}
.pagination button.on{background:var(--accent);color:var(--accent-on);font-weight:600}
.pagination button[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.pagination button svg{width:16px;height:16px;display:block}
.pagination .gap{min-width:36px;text-align:center;color:var(--text-disabled)}
.pagination.compact button{min-width:32px;height:32px;font-size:12px}
.pagination.outline button{border-color:var(--border-default);background:var(--bg-surface)}
.pagination.outline button.on{border-color:var(--accent); color:var(--accent-text);}
.pagination-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;font-size:12px;color:var(--text-secondary)}
.pagination-bar .dropdown .trigger{height:32px;min-width:120px;font-size:12px}