Pagination
Paginationは、コンテンツを複数のページに分割し、ユーザーが異なるページへ移動できるようにするコンポーネントです。
<script lang="ts">
import Pagination from '$lib/components/ui/atoms/Pagination.svelte';
let currentPage = 1;
function onChangePage(page: number) {
currentPage = page;
}
</script>
<Pagination maxPage={10} {currentPage} {onChangePage} />
プロパティ
Paginationは、以下のプロパティをサポートしています。
| 名前 | 型 | デフォルト値 | 説明 |
|---|---|---|---|
maxPage |
number |
0 |
最大ページ数を指定します。 |
maxVisible |
number |
7 |
表示する最大ページ項目数(省略記号含む)。3以下のときは省略記号なしで表示されます。 |
currentPage |
number |
現在のページ番号です。 | |
prevLabel |
string |
前へ |
「前へ」ボタンの表示ラベルです。 |
nextLabel |
string |
次へ |
「次へ」ボタンの表示ラベルです。 |
showEllipsis |
boolean |
true |
省略記号を表示するかどうかを制御します。 |
disabled |
boolean |
false |
指定された場合は選択不可になります。 |
onChangePage |
(page: number) => void |
ページ番号がクリックされた際に呼び出される関数です。 |
インストールの手順
以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。
atoms/Pagination.svelte
<!--
@component
## 概要
- ページネーション UI コンポーネントです。
- 前へ・次へボタン、ページ番号、省略記号(…)を含む表示が可能です。
- 表示する最大ページ数、現在のページ番号、省略記号の表示非表示は外部から制御できます。
## 機能
- 最大ページ数・現在のページを元に、最適なページリンクを自動生成します。
- 中央のページ番号を基準に、常に maxVisible 件に収まるよう調整されます。
- …(省略記号)は maxVisible に含まれ、表示項目数にカウントされます。
- maxVisible <= 3 の場合は省略記号を使わず、現在のページ中心に単純な連番のみを表示します。
- …(省略記号)はクリック不可。ページリンクとしての役割を持ちません。
- 「前へ」「次へ」ナビゲーション付き。
- ページ選択時にはonChangePageが呼び出され、親コンポーネントで状態管理が可能です。
## Props
- maxPage: 最大ページ数を指定します。
- maxVisible: 表示する最大ページ項目数(省略記号含む)。初期値は7。
- currentPage: 現在のページ番号です。
- class: 外部からスタイルを追加できます。
- prevLabel: 「前へ」ボタンのラベル。デフォルトは「前へ」。
- nextLabel: 「次へ」ボタンのラベル。デフォルトは「次へ」。
- showEllipsis: 省略記号の表示位置を設定できます。 none | full。初期値はfull
- disabled: 無効化することができます。デフォルトはfalse
- onChangePage: ページ番号がクリックされたときに呼ばれるコールバック関数です。引数は選択されたページ番号。
## Usage
```svelte
<Pagination max={10} maxVisible={4} {currentPage} {onChangePage} />
```
-->
<script module lang="ts">
import type { ClassValue } from 'svelte/elements';
import { cva, type VariantProps } from 'class-variance-authority';
const MIN_ELLIPSIS_THRESHOLD = 3;
const MAX_VISIBLE = 7;
export const paginationItemVariants = cva('flex items-center justify-center shrink-0 size-10 rounded-md font-medium leading-none text-base-foreground-default text-sm outline-primary transition-colors hover:bg-base-container-accent/90 focus-visible:outline-[0.125rem] focus-visible:outline-offset-[0.125rem] focus-visible:outline-primary', {
variants: {
/** type属性 */
type: {
previous: ['w-auto pr-4 pl-2.5 cursor-pointer'],
next: ['w-auto pr-2.5 pl-4 cursor-pointer'],
link: ['cursor-pointer'],
ellipsis: ['pointer-events-none'],
},
/** 操作できるかどうか */
disabled: {
true: ['opacity-50 pointer-events-none'],
false: [],
},
/** 現在のページかどうか */
current: {
true: ['border border-primary ease-in-out'],
false: ['border border-transparent'],
},
},
defaultVariants: {
disabled: false,
},
});
export type PaginationVariants = VariantProps<typeof paginationItemVariants>;
export interface PaginationProps extends PaginationVariants {
/** 最大ページ数 */
maxPage?: number;
/** 最大表示ページ数 */
maxVisible?: number;
/** 現在のページ番号 */
currentPage?: number;
/** クラス */
class?: ClassValue;
/** 前へボタンのラベル */
prevLabel?: string;
/** 次へボタンのラベル */
nextLabel?: string;
/** 省略記号の表示位置 */
showEllipsis?: boolean;
/** ページを全て無効にするかどうか */
disabled?: boolean;
/** ページが変更されたときに呼ばれる関数 */
onChangePage?: (page: number) => void;
}
</script>
<script lang="ts">
import { ChevronLeft, ChevronRight, Ellipsis } from '@lucide/svelte';
let { class: className, maxPage = 0, maxVisible = MAX_VISIBLE, currentPage = 1, prevLabel = '前へ', nextLabel = '次へ', showEllipsis = true, disabled = false, onChangePage }: PaginationProps = $props();
let pages = $state<(number | 'ellipsis')[]>([]);
$effect(() => {
const last = maxPage;
let dynamic = maxVisible;
// 調整して収まる範囲を探す
while (dynamic >= 1) {
const mid = calculateStartEnd(currentPage, dynamic, last);
const trial = buildPageRange(mid.start, mid.end, last, showEllipsis, maxVisible, currentPage);
if (trial.length <= maxVisible) {
pages = trial;
return;
}
dynamic--;
}
pages = buildPageRange(currentPage, currentPage, last, showEllipsis, maxVisible, currentPage);
});
// 現在ページと可変数から、中央ページ範囲の start と end を計算
function calculateStartEnd(current: number, visible: number, last: number): { start: number; end: number } {
let half = Math.floor(visible / 2);
let start = current - half;
let end = current + half;
// maxVisible が偶数のとき、右に1件多くする
if (visible % 2 === 0) {
start += 1;
}
// 範囲を補正
if (start < 1) {
end += 1 - start;
start = 1;
}
if (end > last) {
start -= end - last;
end = last;
}
// maxVisible 件になるようさらに補正
const count = end - start + 1;
if (count < visible) {
if (start === 1) {
end = Math.min(last, end + (visible - count));
}
else if (end === last) {
start = Math.max(1, start - (visible - count));
}
}
return { start, end };
}
// 省略記号を含めて最終的なページ番号配列を構築
function buildPageRange(
start: number,
end: number,
last: number,
showEllipsis = true,
maxVisible: number,
currentPage: number,
): (number | 'ellipsis')[] {
const result: (number | 'ellipsis')[] = [];
// maxVisible <= 3 のときは中央寄せの単純連番のみ
if (maxVisible <= MIN_ELLIPSIS_THRESHOLD) {
let half = Math.floor(maxVisible / 2);
let simple_start = currentPage - half;
let simple_end = currentPage + half;
if (maxVisible % 2 === 0) simple_start += 1;
if (simple_start < 1) {
simple_end += 1 - simple_start;
simple_start = 1;
}
if (simple_end > last) {
simple_start -= simple_end - last;
simple_end = last;
}
simple_start = Math.max(1, simple_start);
simple_end = Math.min(last, simple_end);
return Array.from({ length: simple_end - simple_start + 1 }, (_, i) => simple_start + i);
}
// showEllipsis = false かつ maxVisible >= 4 のときは常に最初と最後を固定表示
if (!showEllipsis && maxVisible >= 4) {
const inner_count = maxVisible - 2;
const half = Math.floor(inner_count / 2);
let inner_start = currentPage - half;
let inner_end = currentPage + half;
if (inner_count % 2 === 0) inner_start += 1;
if (inner_start < 2) {
inner_end += 2 - inner_start;
inner_start = 2;
}
if (inner_end > last - 1) {
inner_start -= inner_end - (last - 1);
inner_end = last - 1;
}
inner_start = Math.max(inner_start, 2);
inner_end = Math.min(inner_end, last - 1);
result.push(1);
for (let i = inner_start; i <= inner_end; i++) {
result.push(i);
}
result.push(last);
return result;
}
// 通常のellipsisロジック
if (start > 3) {
result.push(1, 'ellipsis');
}
else {
for (let i = 1; i < start; i++) result.push(i);
}
for (let i = start; i <= end; i++) {
result.push(i);
}
if (end < last - 1) {
result.push('ellipsis', last);
}
else {
for (let i = end + 1; i <= last; i++) result.push(i);
}
return result;
}
function findPrevEnabledPage(current: number, disabled: boolean): number | null {
if (disabled) return null;
return current > 1 ? current - 1 : null;
}
function findNextEnabledPage(current: number, max: number, disabled: boolean): number | null {
if (disabled) return null;
return current < max ? current + 1 : null;
}
</script>
<div class={[className, 'flex items-center justify-center gap-0.5']}>
<button
class={paginationItemVariants({ type: 'previous', disabled: !findPrevEnabledPage(currentPage, disabled) })}
onclick={() => {
const prev = findPrevEnabledPage(currentPage, disabled);
if (prev) onChangePage?.(prev);
}}
disabled={!findPrevEnabledPage(currentPage, disabled)}
>
<div class="flex items-center justify-center shrink-0 gap-1"><ChevronLeft size="1rem" />{prevLabel}</div>
</button>
{#each pages as page}
{#if page === 'ellipsis'}
<span class={paginationItemVariants({ type: 'ellipsis', disabled })}>
<Ellipsis size="1rem" />
</span>
{:else}
{#key page}
<button
class={paginationItemVariants({ type: 'link', current: page === currentPage && !disabled, disabled })}
onclick={() => {
if (!disabled) onChangePage?.(page);
}}
{disabled}
aria-current={page === currentPage && !disabled ? 'page' : undefined}
>
{page}
</button>
{/key}
{/if}
{/each}
<button
class={paginationItemVariants({ type: 'next', disabled: !findNextEnabledPage(currentPage, maxPage, disabled) })}
onclick={() => {
const next = findNextEnabledPage(currentPage, maxPage, disabled);
if (next) onChangePage?.(next);
}}
disabled={!findNextEnabledPage(currentPage, maxPage, disabled)}
>
<div class="flex items-center justify-center shrink-0 gap-1"> {nextLabel} <ChevronRight size="1rem" /></div>
</button>
</div>
使い方
<script lang="ts">
import Pagination from '$lib/components/ui/atoms/Pagination.svelte';
let currentPage = 1;
function onChangePage(page: number) {
currentPage = page;
}
</script>
<Pagination maxPage={10} {currentPage} {onChangePage} />
サンプル
Default
基本的なページネーションのスタイルです。前後ボタンとページ番号を表示します。
<script lang="ts">
import Pagination from '$lib/components/ui/atoms/Pagination.svelte';
let currentPage = 1;
function onChangePage(page: number) {
currentPage = page;
}
</script>
<Pagination maxPage={10} {currentPage} {onChangePage} />
Disabled
選択不可の状態です。
<script lang="ts">
import Pagination from '$lib/components/ui/atoms/Pagination.svelte';
</script>
<Pagination maxPage={10} disabled />
OnChangePage
ページ番号をクリックすると onChangePage が発火し、選択されたページ番号を親コンポーネントに通知します。 これにより、外部でページ状態を管理できます。
<script lang="ts">
import DebugConsole from '$lib/components/ui/atoms/DebugConsole.svelte';
import Pagination from '$lib/components/ui/atoms/Pagination.svelte';
let currentPage = 1;
function onChangePage(page: number) {
currentPage = page;
}
</script>
<div class="flex flex-col w-full space-y-4">
<Pagination maxPage={10} {currentPage} {onChangePage} />
<DebugConsole data={currentPage} title="onChangePage に渡された値" />
</div>