Pagination

Splits a long list of data into pages, with page number buttons and previous/next triggers.
Installation
Expand

npx @left4code/cosmic-ui-cli@latest add pagination
 
Expand

This writes the component and everything it imports, then installs the packages it needs. Run npx @left4code/cosmic-ui-cli@latest initfirst if you haven't already (it detects Vue automatically).

Install the following dependencies:

pnpm
npm
yarn
bun
yarn add @zag-js/pagination @zag-js/vue

Built directly on the Zag.js state machine. This is a single self-contained component rather than a Root + parts split - the page buttons come from api.pages, an array the machine computes from count / pageSize / siblingCount, so there's nothing left for a consumer to compose by hand. The plain border shown here is a placeholder; swap it for the Frame primitive later. A single file is enough here (no folder) since there are no child parts to split out. Copy and paste the following code into your project.

components/ui/pagination.vue
Expand

<script setup lang="ts">
import { computed, useAttrs, useId } from "vue";
import { useMachine, normalizeProps } from "@zag-js/vue";
import * as paginationMachine from "@zag-js/pagination";
import { ChevronLeft, ChevronRight } from "@lucide/vue";
import { twMerge } from "tailwind-merge";

defineOptions({ inheritAttrs: false });
const rawAttrs = useAttrs() as Partial<paginationMachine.Props> & { class?: string };
const id = useId();

const machineProps = computed(() => {
  const { class: _class, ...rest } = rawAttrs;
  return { id, ...rest };
});
const service = useMachine(paginationMachine.machine, machineProps);
const api = computed(() => paginationMachine.connect(service, normalizeProps));
</script>

<template>
  <div v-bind="api.getRootProps()" :class="twMerge(['flex items-center gap-1.5', rawAttrs.class])">
    <button
      v-bind="api.getPrevTriggerProps()"
      class="size-8 flex items-center justify-center border border-primary/30 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none"
    >
      <ChevronLeft class="size-4" />
    </button>
    <template v-for="(page, i) in api.pages" :key="i">
      <button
        v-if="page.type === 'page'"
        v-bind="api.getItemProps(page)"
        class="size-8 border border-primary/30 hover:bg-primary/10 data-[selected]:bg-primary/20 data-[selected]:border-primary"
      >
        {{ page.value }}
      </button>
      <span
        v-else
        v-bind="api.getEllipsisProps({ index: i })"
        class="size-8 flex items-center justify-center"
      >
        &#8230;
      </span>
    </template>
    <button
      v-bind="api.getNextTriggerProps()"
      class="size-8 flex items-center justify-center border border-primary/30 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none"
    >
      <ChevronRight class="size-4" />
    </button>
  </div>
</template>
 
Expand

Update the import paths to match your project setup.

Usage
Expand

import Pagination from "@/components/ui/pagination.vue";
 
Expand
Expand

<Pagination :count="100" :defaultPageSize="10" :siblingCount="1" />
 
Expand
Powered by synthetic caffeine · Deployed by Left4code · Signal traceable on GitHub.