Skip to content

@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

PropTypeDefaultDescription
pagenumberrequiredCurrent page, 1-based (v-model:page)
totalPagesnumberrequiredTotal number of pages
siblingsnumber7Max visible page buttons (odd numbers work best)
showPrevNextbooleantrueShow previous/next buttons
prevLabelstring"←"Previous button text
nextLabelstring"→"Next button text
disabledbooleanfalseDisable all interaction
ariaLabelstring"Pagination"Nav element label

Events

EventPayloadDescription
update:pagenumberPage changed

Slots

SlotPropsDescription
prevCustom previous button content
nextCustom next button content
page{ page: number, active: boolean }Custom page button content
ellipsisCustom ellipsis content

Accessibility

  • Wrapped in <nav> with configurable aria-label
  • Active page marked with aria-current="page"
  • Prev/next buttons have aria-label and aria-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

PropertyTypeDescription
itemsComputedRef<PageItem[]>Page items to render
hasPrevComputedRef<boolean>Can go to previous page
hasNextComputedRef<boolean>Can go to next page
hasPagesComputedRef<boolean>Has any pages at all

PageItem Type

ts
type PageItem = { type: "page"; page: number } | { type: "ellipsis"; key: string };

Dependencies

PackagePurpose
@vibe-labs/design-components-paginationCSS tokens + generated styles

Build

bash
npm run build

Built 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>

Vibe