Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Pagination

Pagination with page navigation, next and previous links. Anchor-based so every page is a real, shareable URL.

Playground

  • Previous
  • 1
  • 2
  • 3
  • 4
  • More pages
  • 10
  • Next
—totalPages
—siblings
—showPrevNext
—iconsOnly
Usage
Compose PaginationItems inside PaginationContent. isActive marks the current page — it switches the link to the outline button variant and sets aria-current="page". PaginationEllipsis stands in for collapsed ranges.
  • Previous
  • 1
  • 2
  • 3
  • More pages
  • Next
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@workspace/ui/components/pagination"

<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>
        2
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
Simple
Page links work on their own — drop PaginationPrevious and PaginationNext when space is tight or the list of pages is short.
  • 1
  • 2
  • 3
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>
        2
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
  </PaginationContent>
</Pagination>
Icons only
For compact navigation, render the previous / next controls as plain PaginationLinks — the default size="icon" fits a single caret. Keep an aria-label on each and rtl:rotate-180 on the caret so the arrows mirror in RTL.
  • 1
  • 2
  • 3
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react"

<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationLink href="#" aria-label="Go to previous page">
        <CaretLeftIcon className="rtl:rotate-180" />
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>
        2
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" aria-label="Go to next page">
        <CaretRightIcon className="rtl:rotate-180" />
      </PaginationLink>
    </PaginationItem>
  </PaginationContent>
</Pagination>
Table pagination
The icons-only controls pair naturally with a rows-per-page selector under a data table. Right-align the pagination by overriding the root's mx-auto w-full defaults with mx-0 w-auto.

Page 3 of 12

<div className="flex w-full items-center justify-between">
  <div className="flex items-center gap-2">
    <Label htmlFor="rows-per-page" className="text-sm font-normal">
      Rows per page
    </Label>
    <Select defaultValue="25">
      <SelectTrigger id="rows-per-page" size="sm" className="w-20">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="10">10</SelectItem>
        <SelectItem value="25">25</SelectItem>
        <SelectItem value="50">50</SelectItem>
      </SelectContent>
    </Select>
  </div>
  <div className="flex items-center gap-4">
    <p className="text-sm text-muted-foreground">Page 3 of 12</p>
    <Pagination className="mx-0 w-auto">
      <PaginationContent>
        <PaginationItem>
          <PaginationLink href="#" aria-label="Go to previous page">
            <CaretLeftIcon className="rtl:rotate-180" />
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" aria-label="Go to next page">
            <CaretRightIcon className="rtl:rotate-180" />
          </PaginationLink>
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  </div>
</div>
Controlled
For client-side pagination, hold the page in state and update it in onClick. Anchors have no disabled attribute — disable the bounds with aria-disabled plus pointer-events-none opacity-50.
  • Previous
  • 1
  • 2
  • 3
  • 4
  • 5
  • Next
const totalPages = 5
const [page, setPage] = React.useState(2)

const goTo = (target: number) => (event: React.MouseEvent) => {
  event.preventDefault()
  setPage(Math.min(Math.max(target, 1), totalPages))
}

<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious
        href="#"
        aria-disabled={page === 1 || undefined}
        className={page === 1 ? "pointer-events-none opacity-50" : undefined}
        onClick={goTo(page - 1)}
      />
    </PaginationItem>
    {Array.from({ length: totalPages }, (_, index) => index + 1).map(
      (target) => (
        <PaginationItem key={target}>
          <PaginationLink
            href="#"
            isActive={target === page}
            onClick={goTo(target)}
          >
            {target}
          </PaginationLink>
        </PaginationItem>
      )
    )}
    <PaginationItem>
      <PaginationNext
        href="#"
        aria-disabled={page === totalPages || undefined}
        className={
          page === totalPages ? "pointer-events-none opacity-50" : undefined
        }
        onClick={goTo(page + 1)}
      />
    </PaginationItem>
  </PaginationContent>
</Pagination>
RTL
Set dir="rtl" on the root (or inherit it from the page). The caret icons flip via rtl:rotate-180 and the paddings use logical properties, so previous still points toward the start of the reading direction. The text prop swaps the labels.
  • السابق
  • ١
  • ٢
  • ٣
  • التالي
<Pagination dir="rtl">
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" text="السابق" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">١</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>
        ٢
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">٣</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" text="التالي" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
Next.js
PaginationLink renders a native anchor, so URL-driven pagination works out of the box: point href at the page's query string and read searchParams in the server component. Every page stays a crawlable, shareable URL — no client state needed.
export default async function ProductsPage({
  searchParams,
}: {
  searchParams: Promise<{ page?: string }>
}) {
  const page = Number((await searchParams).page ?? 1)

  return (
    <PaginationItem>
      <PaginationLink href={`?page=${page}`} isActive={page === current}>
        {page}
      </PaginationLink>
    </PaginationItem>
  )
}
Accessibility
The root renders a <nav aria-label="pagination"> landmark wrapping a semantic list, so screen readers announce and skip it as a unit. isActive sets aria-current="page", the ellipsis is aria-hidden with an sr-only "More pages" fallback, and the previous / next controls carry explicit labels. Their text labels hide below sm, falling back to the carets.
<PaginationPrevious
  href="#"
  aria-disabled={isFirstPage || undefined}
  className={isFirstPage ? "pointer-events-none opacity-50" : undefined}
/>
API Reference
Every part extends its underlying element — nav, ul, li, a and span — so anchors accept href, target and friends directly. Links are Buttons under the hood via asChild.
ComponentPropDefault
PaginationLinkisActive?: boolean — outline variant + aria-currentfalse
PaginationLinksize?: Button size"icon"
PaginationPrevioustext?: string — label next to the caret"Previous"
PaginationNexttext?: string — label next to the caret"Next"
PaginationPrevious / Nextsize?: Button size"default"
PaginationEllipsisextends span — aria-hidden, non-interactive—
<Pagination data-slot="pagination" />
<PaginationContent data-slot="pagination-content" />
<PaginationItem data-slot="pagination-item" />
<PaginationLink data-slot="pagination-link" data-active />
<PaginationEllipsis data-slot="pagination-ellipsis" />

Tokens used

10 design tokens consumed by the Pagination component.

  • --background

    Background

    bg-background
  • --foreground

    Foreground

    hover:text-foreground
  • --muted

    Muted

    hover:bg-muteddark:hover:bg-muted/50
  • --border

    Border

    border-border
  • --input

    Input

    dark:border-inputdark:bg-input/30dark:hover:bg-input/50
  • --ring

    Ring

    focus-visible:border-ringfocus-visible:ring-ring/50
  • --radius-md

    Radius 0.8× (8px)

    rounded-md
  • --spacing

    Spacing unit (4px)

    gap-1size-9h-9px-2.5
  • --text-sm

    Text Small (14px)

    text-sm
  • --font-weight-medium

    Font weight Medium (500)

    font-medium