Appearance
@vibe-labs/design-vue-pagination
Vue 3 pagination component for the Vibe Design System. Windowed page range with ellipsis, keyboard accessible, and a standalone composable for headless usage.
Installation
ts
import { VibePagination } from "@vibe-labs/design-vue-pagination";Requires the CSS layer from @vibe-labs/design-components-pagination.
Components
VibePagination
Page navigation with automatic windowing and ellipsis.
Usage
vue
<!-- Basic -->
<VibePagination v-model:page="currentPage" :total-pages="20" />
<!-- Custom visible range -->
<VibePagination v-model:page="currentPage" :total-pages="50" :siblings="9" />
<!-- Without prev/next buttons -->
<VibePagination v-model:page="currentPage" :total-pages="10" :show-prev-next="false" />
<!-- Custom prev/next labels -->
<VibePagination v-model:page="currentPage" :total-pages="10" prev-label="‹" next-label="›" />
<!-- Custom prev/next slots -->
<VibePagination v-model:page="currentPage" :total-pages="10">
<template #prev><ChevronLeftIcon /></template>
<template #next><ChevronRightIcon /></template>
</VibePagination>
<!-- Custom page slot -->
<VibePagination v-model:page="currentPage" :total-pages="10">
<template #page="{ page, active }">
<span :class="{ 'font-bold': active }">{{ page }}</span>
</template>
</VibePagination>
<!-- Disabled -->
<VibePagination v-model:page="currentPage" :total-pages="10" disabled />Windowing Examples
page=1, totalPages=20, siblings=7 → [1, 2, 3, 4, 5, 6, 7, …, 20]
page=5, totalPages=20, siblings=7 → [1, …, 4, 5, 6, 7, 8, …, 20]
page=20, totalPages=20, siblings=7 → [1, …, 14, 15, 16, 17, 18, 19, 20]
page=3, totalPages=5, siblings=7 → [1, 2, 3, 4, 5]Props
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | required | Current page, 1-based (v-model:page) |
totalPages | number | required | Total number of pages |
siblings | number | 7 | Max visible page buttons (odd numbers work best) |
showPrevNext | boolean | true | Show previous/next buttons |
prevLabel | string | "←" | Previous button text |
nextLabel | string | "→" | Next button text |
disabled | boolean | false | Disable all interaction |
ariaLabel | string | "Pagination" | Nav element label |
Events
| Event | Payload | Description |
|---|---|---|
update:page | number | Page changed |
Slots
| Slot | Props | Description |
|---|---|---|
prev | — | Custom previous button content |
next | — | Custom next button content |
page | { page: number, active: boolean } | Custom page button content |
ellipsis | — | Custom ellipsis content |
Accessibility
- Wrapped in
<nav>with configurablearia-label - Active page marked with
aria-current="page" - Prev/next buttons have
aria-labelandaria-disabled - Each page button has
aria-label="Page N"
Composable
usePagination(options)
Headless pagination logic — use when you need the windowing algorithm without the component.
ts
import { usePagination } from "@vibe-labs/design-vue-pagination";
const { items, hasPrev, hasNext, hasPages } = usePagination({
page: toRef(props, "page"),
totalPages: toRef(props, "totalPages"),
siblings: 7,
});Returns
| Property | Type | Description |
|---|---|---|
items | ComputedRef<PageItem[]> | Page items to render |
hasPrev | ComputedRef<boolean> | Can go to previous page |
hasNext | ComputedRef<boolean> | Can go to next page |
hasPages | ComputedRef<boolean> | Has any pages at all |
PageItem Type
ts
type PageItem = { type: "page"; page: number } | { type: "ellipsis"; key: string };Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-pagination | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.
Usage Guide
Setup
ts
import { VibePagination } from "@vibe-labs/design-vue-pagination";
import "@vibe-labs/design-components-pagination";VibePagination — Practical Examples
Basic Pagination with Data Fetching
vue
<script setup lang="ts">
import { ref, watch } from "vue";
import { VibePagination } from "@vibe-labs/design-vue-pagination";
interface User { id: number; name: string }
const page = ref(1);
const totalPages = ref(1);
const users = ref<User[]>([]);
const loading = ref(false);
async function fetchUsers(p: number) {
loading.value = true;
const res = await fetch(`/api/users?page=${p}&perPage=20`);
const data = await res.json();
users.value = data.items;
totalPages.value = data.totalPages;
loading.value = false;
}
watch(page, fetchUsers, { immediate: true });
</script>
<template>
<div>
<ul v-if="!loading">
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<VibePagination v-model:page="page" :total-pages="totalPages" />
</div>
</template>Compact Pagination with Custom Controls
vue
<script setup lang="ts">
import { ref } from "vue";
import { VibePagination } from "@vibe-labs/design-vue-pagination";
import ChevronLeftIcon from "./icons/ChevronLeftIcon.vue";
import ChevronRightIcon from "./icons/ChevronRightIcon.vue";
const page = ref(1);
</script>
<template>
<VibePagination
v-model:page="page"
:total-pages="50"
:siblings="5"
aria-label="Search results"
>
<template #prev><ChevronLeftIcon /></template>
<template #next><ChevronRightIcon /></template>
</VibePagination>
</template>Custom Page Button Rendering
vue
<script setup lang="ts">
import { ref } from "vue";
import { VibePagination } from "@vibe-labs/design-vue-pagination";
const page = ref(1);
</script>
<template>
<VibePagination v-model:page="page" :total-pages="20">
<template #page="{ page: p, active }">
<span :class="['page-btn', { 'page-btn--active': active }]">{{ p }}</span>
</template>
<template #ellipsis>
<span class="page-ellipsis">···</span>
</template>
</VibePagination>
</template>usePagination — Usage Example
vue
<script setup lang="ts">
import { ref } from "vue";
import { usePagination } from "@vibe-labs/design-vue-pagination";
const page = ref(3);
const totalPages = ref(20);
const { items, hasPrev, hasNext } = usePagination({
page,
totalPages,
siblings: 7,
});
</script>
<template>
<nav aria-label="Pagination">
<button :disabled="!hasPrev" @click="page--">Previous</button>
<template v-for="item in items" :key="item.type === 'page' ? item.page : item.key">
<span v-if="item.type === 'ellipsis'">…</span>
<button
v-else
:aria-current="item.page === page ? 'page' : undefined"
@click="page = item.page"
>
{{ item.page }}
</button>
</template>
<button :disabled="!hasNext" @click="page++">Next</button>
</nav>
</template>Common Patterns
Sync Pagination with URL Query Params
vue
<script setup lang="ts">
import { computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import { VibePagination } from "@vibe-labs/design-vue-pagination";
const route = useRoute();
const router = useRouter();
const page = computed({
get: () => Number(route.query.page) || 1,
set: (p) => router.push({ query: { ...route.query, page: p } }),
});
</script>
<template>
<VibePagination v-model:page="page" :total-pages="totalPages" />
</template>Disabled State During Loading
vue
<template>
<VibePagination
v-model:page="page"
:total-pages="totalPages"
:disabled="isLoading"
/>
</template>