Components
Pagination
A <nav> landmark with aria-label; active page carries aria-current="page". Previous/Next pre-labelled so AT users hear the action, not the glyph.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/pagination.json2. Use it
import { Pagination, PaginationItem, PaginationLink,
PaginationPrevious, PaginationNext, PaginationEllipsis } from "@/components/ui/pagination"
<Pagination ariaLabel="Articles">
<PaginationItem><PaginationPrevious href="/articles?page=1" /></PaginationItem>
<PaginationItem><PaginationLink href="/articles?page=1">1</PaginationLink></PaginationItem>
<PaginationItem><PaginationLink active>2</PaginationLink></PaginationItem>
<PaginationItem><PaginationLink href="/articles?page=3">3</PaginationLink></PaginationItem>
<PaginationItem><PaginationEllipsis /></PaginationItem>
<PaginationItem><PaginationNext href="/articles?page=3" /></PaginationItem>
</Pagination>Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
// Pagination — shadcn-htmx, htmx v4 + Tailwind v4.
//
// A standard navigation strip with Previous / 1 / 2 / 3 / … / Next.
// We render a real <nav> landmark with aria-label so AT users can jump
// to it directly. The active page carries aria-current="page" per WAI:
// repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-current/
//
// Server-driven model: each page link is a real <a> with href OR an htmx
// button that swaps a target. The component itself doesn't manage state
// — give it the page numbers + an href/builder + the active page.
type PaginationProps = PropsWithChildren<{
// Accessible label for the navigation landmark.
ariaLabel?: string
class?: ClassValue
}>
export function Pagination(props: PaginationProps) {
const { ariaLabel = "Pagination", class: className, children } = props
return (
<nav
data-slot="pagination"
aria-label={ariaLabel}
class={cn("mx-auto flex w-full justify-center", className)}
>
<ul class="flex flex-row items-center gap-1">{children}</ul>
</nav>
)
}
// Each page (or ellipsis) goes inside an <li>.
type PaginationItemProps = PropsWithChildren<{ class?: ClassValue }>
export function PaginationItem(props: PaginationItemProps) {
return <li class={cn(props.class)}>{props.children}</li>
}
// A single page link. `active` adds aria-current="page" + visual emphasis.
type PaginationLinkProps = PropsWithChildren<{
href?: string
active?: boolean
// HTML link type forwarded to the <a>. Pagination prev/next default this to
// "prev"/"next" — the normative sequence link types:
// repos/whatwg-html/source (link types next/prev)
rel?: string
disabled?: boolean
class?: ClassValue
// htmx attrs ride along.
[key: `hx-${string}`]: any
[key: `data-${string}`]: any
[key: `aria-${string}`]: any
}>
export function PaginationLink(props: PaginationLinkProps) {
const { href, active, rel, disabled, class: className, children, ...rest } = props
const Tag: any = href ? "a" : "button"
return (
<Tag
href={href}
rel={href ? rel : undefined}
// aria-disabled only exposes the state; per the aria-disabled spec it does
// NOT suppress activation or remove focus. On the <button> branch use the
// native disabled attribute so a keyboard user can't fire the hx-* attrs:
// repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-disabled/index.md
disabled={href ? undefined : disabled ? true : undefined}
type={href ? undefined : "button"}
data-slot="pagination-link"
aria-current={active ? "page" : undefined}
class={cn(
"inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none",
active && "bg-primary text-primary-foreground hover:bg-primary/90",
className,
)}
{...rest}
>
{children}
</Tag>
)
}
// Previous / Next chrome — same as PaginationLink but with built-in
// aria-label so screen readers don't say "<" or ">".
type PaginationNavProps = PropsWithChildren<{
href?: string
disabled?: boolean
// rel defaults to "prev"/"next" (the WHATWG sequence link types) but is
// overridable. Widened from hx-* only so anchor attrs can be forwarded.
rel?: string
class?: ClassValue
[key: `hx-${string}`]: any
[key: `data-${string}`]: any
[key: `aria-${string}`]: any
}>
export function PaginationPrevious(props: PaginationNavProps) {
const { href, disabled, rel = "prev", class: className, children, ...rest } = props
return (
<PaginationLink
href={disabled ? undefined : href}
rel={rel}
disabled={disabled}
class={cn("gap-1 pl-2.5", disabled && "pointer-events-none opacity-50", className)}
data-slot="pagination-prev"
aria-label="Previous page"
aria-disabled={disabled ? "true" : undefined}
{...rest}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
<polyline points="15 18 9 12 15 6" />
</svg>
{children ?? <span>Previous</span>}
</PaginationLink>
)
}
export function PaginationNext(props: PaginationNavProps) {
const { href, disabled, rel = "next", class: className, children, ...rest } = props
return (
<PaginationLink
href={disabled ? undefined : href}
rel={rel}
disabled={disabled}
class={cn("gap-1 pr-2.5", disabled && "pointer-events-none opacity-50", className)}
data-slot="pagination-next"
aria-label="Next page"
aria-disabled={disabled ? "true" : undefined}
{...rest}
>
{children ?? <span>Next</span>}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
<polyline points="9 18 15 12 9 6" />
</svg>
</PaginationLink>
)
}
// Decorative ellipsis between page ranges. aria-hidden so AT skips it
// (the page numbers carry the meaning).
export function PaginationEllipsis(props: { class?: ClassValue }) {
return (
<span
data-slot="pagination-ellipsis"
aria-hidden="true"
class={cn("flex h-9 w-9 items-center justify-center text-muted-foreground", props.class)}
>
…
</span>
)
}
1. Save the file
Copy pagination.html into templates/components/.
2. Use it
{% from "components/pagination.html" import pagination_open, pagination_close,
pagination_prev, pagination_next, pagination_page, pagination_ellipsis %}
{{ pagination_open(aria_label="Articles") }}
{{ pagination_prev(href="/articles?page=1") }}
{{ pagination_page(1, href="/articles?page=1") }}
{{ pagination_page(2, active=true) }}
{{ pagination_page(3, href="/articles?page=3") }}
{{ pagination_ellipsis() }}
{{ pagination_next(href="/articles?page=3") }}
{{ pagination_close() }}View source
{# Pagination macros — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/pagination.tsx.
Usage:
{% from "components/pagination.html" import pagination_open, pagination_close,
pagination_prev, pagination_next, pagination_page, pagination_ellipsis %}
{{ pagination_open(aria_label="Articles") }}
{{ pagination_prev(href="/articles?page=1") }}
{{ pagination_page(1, href="/articles?page=1") }}
{{ pagination_page(2, active=true) }}
{{ pagination_page(3, href="/articles?page=3") }}
{{ pagination_ellipsis() }}
{{ pagination_next(href="/articles?page=3") }}
{{ pagination_close() }} #}
{% macro pagination_open(aria_label="Pagination", extra_class="") %}
<nav data-slot="pagination" aria-label="{{ aria_label }}"
class="mx-auto flex w-full justify-center {{ extra_class }}">
<ul class="flex flex-row items-center gap-1">
{% endmacro %}
{% macro pagination_close() %}
</ul>
</nav>
{% endmacro %}
{# rel = WHATWG sequence link type (prev/next), emitted on the <a> branch only.
disabled = native disabled attr on the <button> branch: per the aria-disabled
spec, aria-disabled alone does not suppress keyboard activation, so a disabled
prev/next button gets the real disabled attr too (drops it from tab order and
blocks the hx-* it carries). #}
{% macro _link(label, href=none, active=false, class_="", aria_label=none, aria_disabled=false, rel=none, disabled=false, slot="pagination-link", **attrs) %}
{%- set base -%}
inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none
{%- endset -%}
{%- set on -%}{% if active %}bg-primary text-primary-foreground hover:bg-primary/90{% endif %}{%- endset -%}
{% if href %}
<a href="{{ href }}" data-slot="{{ slot }}" {% if active %}aria-current="page"{% endif %}
{% if aria_label %}aria-label="{{ aria_label }}"{% endif %}
{% if aria_disabled %}aria-disabled="true"{% endif %}
{% if rel %}rel="{{ rel }}"{% endif %}
class="{{ base }} {{ on }} {{ class_ }}"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ label|safe }}</a>
{% else %}
<button type="button" data-slot="{{ slot }}" {% if active %}aria-current="page"{% endif %}
{% if aria_label %}aria-label="{{ aria_label }}"{% endif %}
{% if aria_disabled %}aria-disabled="true"{% endif %}
{% if disabled %}disabled{% endif %}
class="{{ base }} {{ on }} {{ class_ }}"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ label|safe }}</button>
{% endif %}
{% endmacro %}
{% macro pagination_page(n, href=none, active=false, **attrs) %}
<li>{{ _link(n|string, href=href, active=active, **attrs) }}</li>
{% endmacro %}
{% macro pagination_prev(href=none, disabled=false, rel="prev", **attrs) %}
<li>{{ _link('<span aria-hidden="true">‹</span> Previous',
href=none if disabled else href,
class_="gap-1 pl-2.5" + (" pointer-events-none opacity-50" if disabled else ""),
aria_label="Previous page", aria_disabled=disabled, disabled=disabled, rel=rel,
slot="pagination-prev", **attrs) }}</li>
{% endmacro %}
{% macro pagination_next(href=none, disabled=false, rel="next", **attrs) %}
<li>{{ _link('Next <span aria-hidden="true">›</span>',
href=none if disabled else href,
class_="gap-1 pr-2.5" + (" pointer-events-none opacity-50" if disabled else ""),
aria_label="Next page", aria_disabled=disabled, disabled=disabled, rel=rel,
slot="pagination-next", **attrs) }}</li>
{% endmacro %}
{% macro pagination_ellipsis() %}
<li><span data-slot="pagination-ellipsis" aria-hidden="true" class="flex h-9 w-9 items-center justify-center text-muted-foreground">…</span></li>
{% endmacro %}
1. Save the file
Add pagination.tmpl alongside button.tmpl.
2. Use it
{{template "pagination" (dict "AriaLabel" "Articles" "Body" (htmlSafe `
{{template "pagination_prev" (dict "Href" "/articles?page=1")}}
{{template "pagination_page" (dict "N" 1 "Href" "/articles?page=1")}}
{{template "pagination_page" (dict "N" 2 "Active" true)}}
{{template "pagination_ellipsis" (dict)}}
{{template "pagination_next" (dict "Href" "/articles?page=3")}}`))}}View source
{{/* Pagination templates — shadcn-htmx, htmx v4 + Tailwind v4. */}}
{{define "pagination"}}
<nav data-slot="pagination" aria-label="{{or .AriaLabel "Pagination"}}"
class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">{{.Body}}</ul>
</nav>
{{end}}
{{define "pagination_page"}}
{{- $base := "inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" -}}
{{- $on := "" -}}{{- if .Active -}}{{- $on = "bg-primary text-primary-foreground hover:bg-primary/90" -}}{{- end -}}
<li><a href="{{.Href}}" data-slot="pagination-link" {{if .Active}}aria-current="page"{{end}} class="{{$base}} {{$on}}">{{.N}}</a></li>
{{end}}
{{/* Disabled prev/next render as <button disabled> not <a>: aria-disabled alone
does not suppress keyboard activation (aria-disabled spec), and the native
disabled attr is invalid on <a>. rel="prev"/"next" are the WHATWG sequence
link types, emitted on the enabled <a>. Override .Rel to change them. */}}
{{define "pagination_prev"}}
{{- $base := "inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" -}}
{{- $rel := or .Rel "prev" -}}
{{if .Disabled}}<li><button type="button" data-slot="pagination-prev" aria-label="Previous page" aria-disabled="true" disabled class="{{$base}} gap-1 pl-2.5 pointer-events-none opacity-50">‹ Previous</button></li>{{else}}<li><a href="{{.Href}}" data-slot="pagination-prev" aria-label="Previous page" rel="{{$rel}}" class="{{$base}} gap-1 pl-2.5">‹ Previous</a></li>{{end}}
{{end}}
{{define "pagination_next"}}
{{- $base := "inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" -}}
{{- $rel := or .Rel "next" -}}
{{if .Disabled}}<li><button type="button" data-slot="pagination-next" aria-label="Next page" aria-disabled="true" disabled class="{{$base}} gap-1 pr-2.5 pointer-events-none opacity-50">Next ›</button></li>{{else}}<li><a href="{{.Href}}" data-slot="pagination-next" aria-label="Next page" rel="{{$rel}}" class="{{$base}} gap-1 pr-2.5">Next ›</a></li>{{end}}
{{end}}
{{define "pagination_ellipsis"}}
<li><span data-slot="pagination-ellipsis" aria-hidden="true" class="flex h-9 w-9 items-center justify-center text-muted-foreground">…</span></li>
{{end}}
1. Save the file
Drop pagination.ex into lib/my_app_web/components/.
2. Use it
<.pagination aria-label="Articles">
<.pagination_prev href={~p"/articles?page=1"} />
<.pagination_page n={1} href={~p"/articles?page=1"} />
<.pagination_page n={2} active />
<.pagination_page n={3} href={~p"/articles?page=3"} />
<.pagination_ellipsis />
<.pagination_next href={~p"/articles?page=3"} />
</.pagination>View source
defmodule ShadcnHtmx.Components.Pagination do
@moduledoc """
Pagination — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
A `<nav>` landmark with `aria-label`. Active page carries
`aria-current="page"`. Previous/Next have built-in aria-labels
so AT users hear "Previous page" instead of "<".
## Examples
<.pagination aria-label="Articles">
<.pagination_prev href={~p"/articles?page=1"} />
<.pagination_page n={1} href={~p"/articles?page=1"} />
<.pagination_page n={2} active />
<.pagination_page n={3} href={~p"/articles?page=3"} />
<.pagination_ellipsis />
<.pagination_next href={~p"/articles?page=3"} />
</.pagination>
"""
use Phoenix.Component
attr :"aria-label", :string, default: "Pagination"
attr :class, :string, default: nil
slot :inner_block, required: true
def pagination(assigns) do
~H"""
<nav data-slot="pagination" aria-label={assigns[:"aria-label"]}
class={["mx-auto flex w-full justify-center", @class]}>
<ul class="flex flex-row items-center gap-1">{render_slot(@inner_block)}</ul>
</nav>
"""
end
@base "inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors " <>
"hover:bg-accent hover:text-accent-foreground " <>
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"
attr :n, :integer, required: true
attr :href, :string, default: nil
attr :active, :boolean, default: false
attr :class, :string, default: nil
attr :rest, :global
def pagination_page(assigns) do
on = if assigns.active, do: "bg-primary text-primary-foreground hover:bg-primary/90", else: ""
assigns = assign(assigns, on: on, base: @base)
~H"""
<li>
<a
href={@href}
data-slot="pagination-link"
aria-current={if @active, do: "page"}
class={[@base, @on, @class]}
{@rest}
>
{@n}
</a>
</li>
"""
end
attr :href, :string, default: nil
attr :disabled, :boolean, default: false
# WHATWG sequence link type, emitted on the enabled <a>. Overridable.
attr :rel, :string, default: "prev"
attr :class, :string, default: nil
attr :rest, :global
def pagination_prev(assigns) do
assigns = assign(assigns, base: @base)
# Disabled renders a native <button disabled>, not an <a>: per the
# aria-disabled spec, aria-disabled alone does not suppress keyboard
# activation, and the native disabled attr is invalid on <a>.
~H"""
<li>
<button
:if={@disabled}
type="button"
data-slot="pagination-prev"
aria-label="Previous page"
aria-disabled="true"
disabled
class={[@base, "gap-1 pl-2.5 pointer-events-none opacity-50", @class]}
{@rest}
>
‹ Previous
</button>
<a
:if={!@disabled}
href={@href}
data-slot="pagination-prev"
aria-label="Previous page"
rel={@rel}
class={[@base, "gap-1 pl-2.5", @class]}
{@rest}
>
‹ Previous
</a>
</li>
"""
end
attr :href, :string, default: nil
attr :disabled, :boolean, default: false
attr :rel, :string, default: "next"
attr :class, :string, default: nil
attr :rest, :global
def pagination_next(assigns) do
assigns = assign(assigns, base: @base)
~H"""
<li>
<button
:if={@disabled}
type="button"
data-slot="pagination-next"
aria-label="Next page"
aria-disabled="true"
disabled
class={[@base, "gap-1 pr-2.5 pointer-events-none opacity-50", @class]}
{@rest}
>
Next ›
</button>
<a
:if={!@disabled}
href={@href}
data-slot="pagination-next"
aria-label="Next page"
rel={@rel}
class={[@base, "gap-1 pr-2.5", @class]}
{@rest}
>
Next ›
</a>
</li>
"""
end
def pagination_ellipsis(assigns) do
~H"""
<li>
<span data-slot="pagination-ellipsis" aria-hidden="true"
class="flex h-9 w-9 items-center justify-center text-muted-foreground">…</span>
</li>
"""
end
end
1. Save the file
Tailwind utilities only; no JS.
2. Use it
<nav aria-label="Articles" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li><a href="/articles?page=1" aria-label="Previous page" class="…">‹ Previous</a></li>
<li><a href="/articles?page=1" class="…">1</a></li>
<li><a aria-current="page" class="… bg-primary text-primary-foreground">2</a></li>
<li><a href="/articles?page=3" aria-label="Next page" class="…">Next ›</a></li>
</ul>
</nav>View source
<!--
shadcn-htmx — raw HTML pagination snippet.
<nav> landmark with aria-label, aria-current="page" on the active link,
built-in aria-labels for previous / next.
-->
<nav data-slot="pagination" aria-label="Articles"
class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<!-- rel="prev"/"next": WHATWG sequence link types for paged content. -->
<a href="/articles?page=1" data-slot="pagination-prev" aria-label="Previous page" rel="prev"
class="inline-flex h-9 min-w-9 items-center justify-center gap-1 rounded-md px-3 pl-2.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">
‹ Previous
</a>
</li>
<li>
<a href="/articles?page=1" data-slot="pagination-link"
class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">1</a>
</li>
<li>
<a href="/articles?page=2" data-slot="pagination-link" aria-current="page"
class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none bg-primary text-primary-foreground hover:bg-primary/90">2</a>
</li>
<li>
<a href="/articles?page=3" data-slot="pagination-link"
class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">3</a>
</li>
<li>
<span data-slot="pagination-ellipsis" aria-hidden="true"
class="flex h-9 w-9 items-center justify-center text-muted-foreground">…</span>
</li>
<li>
<a href="/articles?page=3" data-slot="pagination-next" aria-label="Next page" rel="next"
class="inline-flex h-9 min-w-9 items-center justify-center gap-1 rounded-md px-3 pr-2.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">
Next ›
</a>
</li>
</ul>
</nav>
Examples
Basic — static links
Each page is a real <a href> so it works without JS and is bookmarkable / shareable.
The <nav> landmark + aria-label lets AT users jump straight to it from the landmarks menu. aria-current="page" is the WAI-recommended way to mark the active page — styling alone isn't enough.
import { Pagination, PaginationItem, PaginationLink,
PaginationPrevious, PaginationNext, PaginationEllipsis } from "@/components/ui/pagination"
<Pagination ariaLabel="Articles">
<PaginationItem><PaginationPrevious href="/articles?page=1" /></PaginationItem>
<PaginationItem><PaginationLink href="/articles?page=1">1</PaginationLink></PaginationItem>
<PaginationItem><PaginationLink active>2</PaginationLink></PaginationItem>
<PaginationItem><PaginationLink href="/articles?page=3">3</PaginationLink></PaginationItem>
<PaginationItem><PaginationEllipsis /></PaginationItem>
<PaginationItem><PaginationNext href="/articles?page=3" /></PaginationItem>
</Pagination>{% from "components/pagination.html" import pagination_open, pagination_close,
pagination_prev, pagination_next, pagination_page, pagination_ellipsis %}
{{ pagination_open(aria_label="Articles") }}
{{ pagination_prev(href="/articles?page=1") }}
{{ pagination_page(1, href="/articles?page=1") }}
{{ pagination_page(2, active=true) }}
{{ pagination_page(3, href="/articles?page=3") }}
{{ pagination_ellipsis() }}
{{ pagination_next(href="/articles?page=3") }}
{{ pagination_close() }}{{template "pagination" (dict "AriaLabel" "Articles" "Body" (htmlSafe `
{{template "pagination_prev" (dict "Href" "/articles?page=1")}}
{{template "pagination_page" (dict "N" 1 "Href" "/articles?page=1")}}
{{template "pagination_page" (dict "N" 2 "Active" true)}}
{{template "pagination_ellipsis" (dict)}}
{{template "pagination_next" (dict "Href" "/articles?page=3")}}`))}}<.pagination aria-label="Articles">
<.pagination_prev href={~p"/articles?page=1"} />
<.pagination_page n={1} href={~p"/articles?page=1"} />
<.pagination_page n={2} active />
<.pagination_page n={3} href={~p"/articles?page=3"} />
<.pagination_ellipsis />
<.pagination_next href={~p"/articles?page=3"} />
</.pagination><nav data-slot="pagination" aria-label="Articles" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li class="">
<a href="?page=1" rel="prev" data-slot="pagination-prev" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none gap-1 pl-2.5" aria-label="Previous page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
<polyline points="15 18 9 12 15 6">
</polyline>
</svg>
<span>Previous</span>
</a>
</li>
<li class="">
<a href="?page=1" data-slot="pagination-link" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">1</a>
</li>
<li class="">
<button type="button" data-slot="pagination-link" aria-current="page" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none bg-primary text-primary-foreground hover:bg-primary/90">2</button>
</li>
<li class="">
<a href="?page=3" data-slot="pagination-link" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none">3</a>
</li>
<li class="">
<span data-slot="pagination-ellipsis" aria-hidden="true" class="flex h-9 w-9 items-center justify-center text-muted-foreground">…</span>
</li>
<li class="">
<a href="?page=3" rel="next" data-slot="pagination-next" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none gap-1 pr-2.5" aria-label="Next page">
<span>Next</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
<polyline points="9 18 15 12 9 6">
</polyline>
</svg>
</a>
</li>
</ul>
</nav>Further reading
htmx — partial swap, no full reload
Each page link is hx-get + hx-target. The server returns just the content + new pagination, htmx swaps innerHTML.
Click around — the URL doesn't change but the content (and the highlighted page) does. For deep links pair with hx-push-url so back/forward + bookmarking still work.
Showing page 1 of 5
- Article 1 — Intro to htmx
- Article 2 — Hypermedia controls
- Article 3 — Server-rendered SPAs
<PaginationLink active={n === active}
hx-get={`/api/articles?page=${n}`}
hx-target="#article-list"
hx-swap="innerHTML"
>{n}</PaginationLink>{{ pagination_page(n, active=(n == active),
hx_get="/api/articles?page=" ~ n,
hx_target="#article-list",
hx_swap="innerHTML") }}{{template "pagination_page" (dict "N" $n "Active" (eq $n .Active)
"Href" "/api/articles?page=…" )}}<.pagination_page n={n} active={n == @active}
hx-get={~p"/api/articles?page=#{n}"}
hx-target="#article-list"
hx-swap="innerHTML" /><div id="ex-pag-host" class="w-full max-w-2xl">
<div aria-live="polite" class="mb-4 grid gap-3 rounded-lg border p-4 text-sm">
<p class="font-medium">Showing page 1 of 5</p>
<ul class="grid gap-1 text-muted-foreground">
<li>Article 1 — Intro to htmx</li>
<li>Article 2 — Hypermedia controls</li>
<li>Article 3 — Server-rendered SPAs</li>
</ul>
</div>
<nav data-slot="pagination" aria-label="Demo articles" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li class="">
<button disabled="" type="button" data-slot="pagination-prev" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none gap-1 pl-2.5 pointer-events-none opacity-50" aria-label="Previous page" aria-disabled="true" hx-get="/pagination/page?page=1" hx-target="#ex-pag-host" hx-swap="innerHTML">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
<polyline points="15 18 9 12 15 6">
</polyline>
</svg>
<span>Previous</span>
</button>
</li>
<li class="">
<button type="button" data-slot="pagination-link" aria-current="page" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none bg-primary text-primary-foreground hover:bg-primary/90" data-test="page-1" hx-get="/pagination/page?page=1" hx-target="#ex-pag-host" hx-swap="innerHTML">1</button>
</li>
<li class="">
<button type="button" data-slot="pagination-link" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" data-test="page-2" hx-get="/pagination/page?page=2" hx-target="#ex-pag-host" hx-swap="innerHTML">2</button>
</li>
<li class="">
<button type="button" data-slot="pagination-link" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" data-test="page-3" hx-get="/pagination/page?page=3" hx-target="#ex-pag-host" hx-swap="innerHTML">3</button>
</li>
<li class="">
<button type="button" data-slot="pagination-link" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" data-test="page-4" hx-get="/pagination/page?page=4" hx-target="#ex-pag-host" hx-swap="innerHTML">4</button>
</li>
<li class="">
<button type="button" data-slot="pagination-link" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" data-test="page-5" hx-get="/pagination/page?page=5" hx-target="#ex-pag-host" hx-swap="innerHTML">5</button>
</li>
<li class="">
<button type="button" data-slot="pagination-next" class="inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none gap-1 pr-2.5" aria-label="Next page" hx-get="/pagination/page?page=2" hx-target="#ex-pag-host" hx-swap="innerHTML">
<span>Next</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
<polyline points="9 18 15 12 9 6">
</polyline>
</svg>
</button>
</li>
</ul>
</nav>
</div>Further reading
API Reference
<Pagination>
| Prop | Type | Default | Description |
|---|---|---|---|
rel | string | "prev" / "next" | Forwarded to the Previous/Next <a>. Defaults to the WHATWG sequence link types rel="prev" / rel="next"; overridable. Only emitted on the enabled anchor (dropped when disabled renders a <button>).WHATWGLink types: prev/next |
disabled | boolean | false | Dead-ends Previous/Next at the first/last page. Drops href and renders a native <button disabled> (removed from tab order, activation suppressed) while keeping aria-disabled="true" — aria-disabled alone does not block keyboard activation.MDNaria-disabled |
ariaLabel | string | "Pagination" | Landmark name for the <nav>. |
class | string | — | Extra Tailwind classes appended to the root element. |