Pagination
Menus & NavigationLocal guide ↗
import { Pagination } from "bits-ui" Controlled results
bind:page stays synchronized with page triggers and external controls
Audit log
Current page: 6
Showing 101–120 of 247 events
<script lang="ts">
import { Pagination } from 'bits-ui';
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
let page = $state(6);
const count = 247;
const perPage = 20;
const pageClass =
'inline-flex size-9 items-center justify-center rounded-lg text-sm font-medium text-ink-200 outline-none hover:bg-ink-800 data-[selected]:bg-brand-600 data-[selected]:text-white focus-visible:ring-2 focus-visible:ring-brand-400';
</script>
<div class="space-y-5">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<p class="text-sm font-medium text-ink-50">Audit log</p>
<p class="text-xs text-ink-400">Current page: <span class="font-mono text-brand-400">{page}</span></p>
</div>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" onclick={() => (page = 1)}>First page</button>
<button class="btn btn-ghost btn-sm" onclick={() => (page = Math.ceil(count / perPage))}>Last page</button>
</div>
</div>
<Pagination.Root {count} {perPage} bind:page siblingCount={1}>
{#snippet children({ pages, range })}
<div class="flex flex-wrap items-center justify-between gap-4 border-t border-ink-800 pt-4">
<p class="text-xs text-ink-400">
Showing <span class="text-ink-200">{range.start}–{range.end}</span> of {count} events
</p>
<div class="flex items-center gap-1">
<Pagination.PrevButton class="btn btn-ghost btn-sm px-2" aria-label="Previous page">
<ChevronLeft class="size-4" />
</Pagination.PrevButton>
{#each pages as pageItem (pageItem.key)}
{#if pageItem.type === 'ellipsis'}
<span class="inline-flex size-9 items-center justify-center text-sm text-ink-600" aria-hidden="true">…</span>
{:else}
<Pagination.Page page={pageItem} class={pageClass}>{pageItem.value}</Pagination.Page>
{/if}
{/each}
<Pagination.NextButton class="btn btn-ghost btn-sm px-2" aria-label="Next page">
<ChevronRight class="size-4" />
</Pagination.NextButton>
</div>
</div>
{/snippet}
</Pagination.Root>
</div>
Compact
siblingCount=0 keeps the control useful in a narrow toolbar
……
Page 3 of 12
<script lang="ts">
import { Pagination } from 'bits-ui';
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
let page = $state(3);
</script>
<div class="mx-auto max-w-md rounded-xl border border-ink-800 bg-ink-900 p-3">
<Pagination.Root count={96} perPage={8} bind:page siblingCount={0}>
{#snippet children({ pages, currentPage })}
<div class="flex items-center justify-between gap-3">
<Pagination.PrevButton class="btn btn-ghost btn-sm gap-1">
<ChevronLeft class="size-4" /> Previous
</Pagination.PrevButton>
<div class="flex items-center gap-1">
{#each pages as pageItem (pageItem.key)}
{#if pageItem.type === 'ellipsis'}
<span class="px-1 text-xs text-ink-600">…</span>
{:else}
<Pagination.Page
page={pageItem}
class="inline-flex size-8 items-center justify-center rounded-md text-xs text-ink-400 hover:bg-ink-800 data-[selected]:bg-brand-600 data-[selected]:text-white"
>
{pageItem.value}
</Pagination.Page>
{/if}
{/each}
</div>
<Pagination.NextButton class="btn btn-ghost btn-sm gap-1">
Next <ChevronRight class="size-4" />
</Pagination.NextButton>
</div>
<p class="mt-2 text-center text-[0.7rem] text-ink-600">Page {currentPage} of 12</p>
{/snippet}
</Pagination.Root>
</div>
count is the required total item count; pagination derives its page count from perPage, which defaults to 1. The children snippet receives pages, range, and currentPage, so summary text and controls share the same calculation.
Entries in pages are a discriminated union: render type="page" with Pagination.Page, but render type="ellipsis" yourself. Horizontal and vertical orientation also determine the component’s arrow-key behavior.