Pagination
Page navigation links, composing Button's variants. Purely presentational.
Installation
npx kosmesis add paginationpnpm dlx kosmesis add paginationyarn dlx kosmesis add paginationbunx kosmesis add paginationInstall the following dependencies:
npm install @morphos/iconspnpm add @morphos/iconsyarn add @morphos/iconsbun add @morphos/iconsThis 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.
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/iconspnpm add @praxisjs/css @morphos/iconsyarn add @praxisjs/css @morphos/iconsbun add @praxisjs/css @morphos/iconsThis 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.
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
PaginationLink
| Prop | Type | Default |
|---|---|---|
href | string | — |
isActive | boolean | — |
size | "icon" | "default" | "icon" |
onClick | (event: MouseEvent) => void | — |
aria-label | string | — |
PaginationPrevious / PaginationNext
Thin wrappers around PaginationLink with size fixed to "default" and a hardcoded
aria-label ("Go to previous/next page").
| Prop | Type | Default |
|---|---|---|
href | string | — |
onClick | (event: MouseEvent) => void | — |
children | Children | "Previous" / "Next" |
Pagination, PaginationContent, PaginationItem, and PaginationEllipsis take no props
beyond class/children.