Kosmesis
Components

Pagination

Page navigation links, composing Button's variants. Purely presentational.

Installation

npx kosmesis add pagination
pnpm dlx kosmesis add pagination
yarn dlx kosmesis add pagination
bunx kosmesis add pagination

Install the following dependencies:

npm install @morphos/icons
pnpm add @morphos/icons
yarn add @morphos/icons
bun add @morphos/icons

This component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button.

Copy and paste the following code into your project.

pagination.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { buttonVariants } from "./button";import { cn } from "@/lib/utils";export interface PaginationProps {  class?: string;  children?: Children;}@Component()export class Pagination extends StatelessComponent<PaginationProps> {  render() {    const { class: cls, children } = this.props;    return (      <nav role="navigation" aria-label="pagination" class={cn("mx-auto flex w-full justify-center", cls)}>        {children}      </nav>    );  }}export interface PaginationContentProps {  class?: string;  children?: Children;}@Component()export class PaginationContent extends StatelessComponent<PaginationContentProps> {  render() {    const { class: cls, children } = this.props;    return <ul class={cn("flex flex-row items-center gap-1", cls)}>{children}</ul>;  }}@Component()export class PaginationItem extends StatelessComponent<PaginationContentProps> {  render() {    const { class: cls, children } = this.props;    return <li class={cls}>{children}</li>;  }}export interface PaginationLinkProps {  href?: string;  isActive?: boolean;  size?: "icon" | "default";  onClick?: (event: MouseEvent) => void;  class?: string;  "aria-label"?: string;  children?: Children;}@Component()export class PaginationLink extends StatelessComponent<PaginationLinkProps> {  render() {    const { href, isActive, size = "icon", onClick, class: cls, "aria-label": ariaLabel, children } = this.props;    return (      <a        href={href}        aria-current={isActive ? "page" : undefined}        aria-label={ariaLabel}        onClick={onClick}        class={cn(buttonVariants({ variant: isActive ? "outline" : "ghost", size }), cls)}      >        {children}      </a>    );  }}@Component()export class PaginationPrevious extends StatelessComponent<Omit<PaginationLinkProps, "size" | "isActive">> {  render() {    const { href, onClick, class: cls, children } = this.props;    return (      <PaginationLink href={href} onClick={onClick} aria-label="Go to previous page" size="default" class={cn("gap-1 pl-2.5", cls)}>        <Icon name="ChevronLeft" size={16} />        {children ?? "Previous"}      </PaginationLink>    );  }}@Component()export class PaginationNext extends StatelessComponent<Omit<PaginationLinkProps, "size" | "isActive">> {  render() {    const { href, onClick, class: cls, children } = this.props;    return (      <PaginationLink href={href} onClick={onClick} aria-label="Go to next page" size="default" class={cn("gap-1 pr-2.5", cls)}>        {children ?? "Next"}        <Icon name="ChevronRight" size={16} />      </PaginationLink>    );  }}@Component()export class PaginationEllipsis extends StatelessComponent<{ class?: string }> {  render() {    const { class: cls } = this.props;    return (      <span aria-hidden={"true" as const} class={cn("flex size-9 items-center justify-center", cls)}>        <Icon name="Ellipsis" size={16} />        <span class="sr-only">More pages</span>      </span>    );  }}

Install the following dependencies:

npm install @praxisjs/css @morphos/icons
pnpm add @praxisjs/css @morphos/icons
yarn add @praxisjs/css @morphos/icons
bun add @praxisjs/css @morphos/icons

This component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button.

Copy and paste the following code into your project.

pagination.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { ButtonStyles } from "./button";class PaginationStyles extends Stylesheet {  $root = this.css({ marginLeft: "auto", marginRight: "auto", display: "flex", width: "100%", justifyContent: "center" });  $content = this.css({ display: "flex", flexDirection: "row", alignItems: "center", gap: "0.25rem" });  $previous = this.css({ gap: "0.25rem", paddingLeft: "0.625rem" });  $next = this.css({ gap: "0.25rem", paddingRight: "0.625rem" });  $ellipsis = this.css({ display: "flex", width: "2.25rem", height: "2.25rem", alignItems: "center", justifyContent: "center" });  /** Visually-hidden-but-accessible text — the `@praxisjs/css` equivalent of Tailwind's `sr-only` utility. */  $srOnly = this.css({    position: "absolute",    width: "1px",    height: "1px",    padding: "0",    margin: "-1px",    overflow: "hidden",    clip: "rect(0, 0, 0, 0)",    whiteSpace: "nowrap",    border: "0",  });}export interface PaginationProps {  class?: string;  children?: Children;}@Component()export class Pagination extends StatelessComponent<PaginationProps> {  @Styled(PaginationStyles) $s!: PaginationStyles;  render() {    const { class: cls, children } = this.props;    return (      <nav role="navigation" aria-label="pagination" class={cx(this.$s.$root, cls)}>        {children}      </nav>    );  }}export interface PaginationContentProps {  class?: string;  children?: Children;}@Component()export class PaginationContent extends StatelessComponent<PaginationContentProps> {  @Styled(PaginationStyles) $s!: PaginationStyles;  render() {    const { class: cls, children } = this.props;    return <ul class={cx(this.$s.$content, cls)}>{children}</ul>;  }}@Component()export class PaginationItem extends StatelessComponent<PaginationContentProps> {  render() {    const { class: cls, children } = this.props;    return <li class={cls}>{children}</li>;  }}export interface PaginationLinkProps {  href?: string;  isActive?: boolean;  size?: "icon" | "default";  onClick?: (event: MouseEvent) => void;  class?: string;  "aria-label"?: string;  children?: Children;}@Component()export class PaginationLink extends StatelessComponent<PaginationLinkProps> {  @Styled(ButtonStyles) $btn!: ButtonStyles;  render() {    const { href, isActive, size = "icon", onClick, class: cls, "aria-label": ariaLabel, children } = this.props;    const variant = isActive ? this.$btn.$variantOutline : this.$btn.$variantGhost;    const sizeClass = size === "icon" ? this.$btn.$sizeIcon : this.$btn.$sizeDefault;    return (      <a        href={href}        aria-current={isActive ? "page" : undefined}        aria-label={ariaLabel}        onClick={onClick}        class={cx(this.$btn.$root, variant, sizeClass, cls)}      >        {children}      </a>    );  }}@Component()export class PaginationPrevious extends StatelessComponent<Omit<PaginationLinkProps, "size" | "isActive">> {  @Styled(PaginationStyles) $s!: PaginationStyles;  render() {    const { href, onClick, class: cls, children } = this.props;    return (      <PaginationLink href={href} onClick={onClick} aria-label="Go to previous page" size="default" class={cx(this.$s.$previous, cls)}>        <Icon name="ChevronLeft" size={16} />        {children ?? "Previous"}      </PaginationLink>    );  }}@Component()export class PaginationNext extends StatelessComponent<Omit<PaginationLinkProps, "size" | "isActive">> {  @Styled(PaginationStyles) $s!: PaginationStyles;  render() {    const { href, onClick, class: cls, children } = this.props;    return (      <PaginationLink href={href} onClick={onClick} aria-label="Go to next page" size="default" class={cx(this.$s.$next, cls)}>        {children ?? "Next"}        <Icon name="ChevronRight" size={16} />      </PaginationLink>    );  }}@Component()export class PaginationEllipsis extends StatelessComponent<{ class?: string }> {  @Styled(PaginationStyles) $s!: PaginationStyles;  render() {    const { class: cls } = this.props;    return (      <span aria-hidden={"true" as const} class={cx(this.$s.$ellipsis, cls)}>        <Icon name="Ellipsis" size={16} />        <span class={this.$s.$srOnly}>More pages</span>      </span>    );  }}

Examples

Usage

import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination";

<Pagination>
  <PaginationContent>
    <PaginationItem><PaginationPrevious href="#" /></PaginationItem>
    <PaginationItem><PaginationLink href="#" isActive>1</PaginationLink></PaginationItem>
    <PaginationItem><PaginationLink href="#">2</PaginationLink></PaginationItem>
    <PaginationItem><PaginationEllipsis /></PaginationItem>
    <PaginationItem><PaginationNext href="#" /></PaginationItem>
  </PaginationContent>
</Pagination>

Props

PropTypeDefault
hrefstring
isActiveboolean
size"icon" | "default""icon"
onClick(event: MouseEvent) => void
aria-labelstring

PaginationPrevious / PaginationNext

Thin wrappers around PaginationLink with size fixed to "default" and a hardcoded aria-label ("Go to previous/next page").

PropTypeDefault
hrefstring
onClick(event: MouseEvent) => void
childrenChildren"Previous" / "Next"

Pagination, PaginationContent, PaginationItem, and PaginationEllipsis take no props beyond class/children.

On this page