Skip to content

Pagination

The Pagination component creates Bootstrap page navigation for tables, search results, card feeds, and HTMX-powered partial updates.

Quick Start

from faststrap import Pagination

Pagination(current_page=2, total_pages=10)

Common Patterns

Compact Table Pagination

Pagination(
    current_page=page,
    total_pages=total_pages,
    size="sm",
    align="end",
)

First, Previous, Next, Last Controls

Pagination(
    current_page=2,
    total_pages=100,
    show_first_last=True,  # adds << and >> controls
    show_prev_next=True,   # adds < and > controls
)

Preserve Search And Filter Query Params

Use query_params when pagination links need to keep the current filter/search state.

Pagination(
    current_page=page,
    total_pages=total_pages,
    base_url="/products",
    query_params={"q": query, "category": category},
    page_param="page",
)

HTMX Pagination

Set htmx=True to render hx-get links while still keeping normal href values as a fallback.

Pagination(
    current_page=page,
    total_pages=total_pages,
    base_url="/products",
    query_params={"q": query},
    htmx=True,
    hx_target="#product-list",
    hx_swap="outerHTML",
    hx_push_url=True,
)

A matching handler can return the updated list and pagination fragment:

@app.get("/products")
def products(q: str = "", page: int = 1):
    items, total_pages = search_products(q=q, page=page)
    return Div(
        product_grid(items),
        Pagination(
            current_page=page,
            total_pages=total_pages,
            base_url="/products",
            query_params={"q": q},
            htmx=True,
            hx_target="#product-list",
            hx_swap="outerHTML",
            hx_push_url=True,
        ),
        id="product-list",
    )

Responsive Pattern

from faststrap import Div, Pagination


def ResponsivePagination(current: int, total: int):
    return Div(
        Pagination(
            current_page=current,
            total_pages=total,
            max_pages=7,
            show_first_last=True,
            cls="d-none d-md-flex",
        ),
        Pagination(
            current_page=current,
            total_pages=total,
            max_pages=3,
            show_first_last=False,
            size="sm",
            cls="d-md-none",
        ),
    )

Defaults

from faststrap import Pagination, set_component_defaults

set_component_defaults(
    "Pagination",
    align="center",
    show_first_last=True,
    max_pages=7,
)

Pagination(current_page=5, total_pages=20)

Accessibility

Faststrap handles the important pagination semantics:

  • aria-label on the outer <nav>
  • aria-current="page" on the active page
  • aria-label on first/previous/next/last controls
  • disabled links for unavailable controls

Use a more specific label when the page has more than one paginator:

Pagination(
    current_page=5,
    total_pages=20,
    aria_label="Product listing navigation",
)

Parameter Reference

Parameter Type Default Description
current_page int Required Current active page, 1-indexed
total_pages int Required Total number of pages
size "sm" | "lg" | None None Bootstrap pagination size
align "start" | "center" | "end" "start" Alignment of the pagination list
max_pages int | None 5 Maximum numbered page links to show
base_url str | None "#" Base URL used for page links
page_param str "page" Query parameter name for page numbers
query_params dict[str, Any] | None None Extra query params to preserve in generated links
show_first_last bool | None False Show << and >> controls
show_prev_next bool | None True Show < and > controls
htmx bool False Add HTMX attributes to page links
hx_target str | None None Target for HTMX page updates
hx_swap str | None "outerHTML" HTMX swap strategy
hx_push_url bool False Push generated URLs into browser history
**kwargs Any - Additional HTML attributes such as cls, id, or aria_label

faststrap.components.navigation.pagination.Pagination(current_page, total_pages, size=UNSET, align=UNSET, max_pages=UNSET, base_url=UNSET, show_first_last=UNSET, show_prev_next=UNSET, page_param='page', query_params=None, htmx=False, hx_target=None, hx_swap='outerHTML', hx_push_url=False, **kwargs)

Bootstrap Pagination component for page navigation.

Parameters:

Name Type Description Default
current_page int

Current active page (1-indexed)

required
total_pages int

Total number of pages

required
size SizeType | None

Pagination size (sm, lg)

UNSET
align AlignType | None

Alignment (start, center, end)

UNSET
max_pages int | None

Maximum page numbers to show

UNSET
base_url str | None

Base URL for page links

UNSET
show_first_last bool | None

Show first/last page buttons

UNSET
show_prev_next bool | None

Show previous/next buttons

UNSET
page_param str

Query-string parameter used for page links

'page'
query_params dict[str, Any] | None

Extra query parameters to preserve

None
htmx bool

Add hx-get attributes to generated page links

False
hx_target str | None

Optional HTMX target for nav and generated links

None
hx_swap str | None

Optional HTMX swap mode for generated links

'outerHTML'
hx_push_url bool

Push generated URLs when HTMX links are clicked

False
**kwargs Any

Additional HTML attributes

{}
Source code in src/faststrap/components/navigation/pagination.py
@register(category="navigation")
def Pagination(
    current_page: int,
    total_pages: int,
    size: SizeType | None = UNSET,
    align: AlignType | None = UNSET,
    max_pages: int | None = UNSET,
    base_url: str | None = UNSET,
    show_first_last: bool | None = UNSET,
    show_prev_next: bool | None = UNSET,
    page_param: str = "page",
    query_params: dict[str, Any] | None = None,
    htmx: bool = False,
    hx_target: str | None = None,
    hx_swap: str | None = "outerHTML",
    hx_push_url: bool = False,
    **kwargs: Any,
) -> Nav:
    """Bootstrap Pagination component for page navigation.

    Args:
        current_page: Current active page (1-indexed)
        total_pages: Total number of pages
        size: Pagination size (sm, lg)
        align: Alignment (start, center, end)
        max_pages: Maximum page numbers to show
        base_url: Base URL for page links
        show_first_last: Show first/last page buttons
        show_prev_next: Show previous/next buttons
        page_param: Query-string parameter used for page links
        query_params: Extra query parameters to preserve
        htmx: Add hx-get attributes to generated page links
        hx_target: Optional HTMX target for nav and generated links
        hx_swap: Optional HTMX swap mode for generated links
        hx_push_url: Push generated URLs when HTMX links are clicked
        **kwargs: Additional HTML attributes
    """
    cfg = resolve_defaults(
        "Pagination",
        size=size,
        align=align,
        max_pages=max_pages,
        base_url=base_url,
        show_first_last=show_first_last,
        show_prev_next=show_prev_next,
    )

    c_size = cfg.get("size")
    c_align = cfg.get("align", "start")
    c_max_pages = cfg.get("max_pages", 5)
    c_base_url = cfg.get("base_url", "#")
    c_show_first_last = cfg.get("show_first_last", False)
    c_show_prev_next = cfg.get("show_prev_next", True)

    classes = ["pagination"]
    if c_size:
        classes.append(f"pagination-{c_size}")

    justify_class = {
        "center": "justify-content-center",
        "end": "justify-content-end",
    }.get(c_align)

    user_cls = kwargs.pop("cls", "")
    ul_cls = merge_classes(" ".join(classes), user_cls)

    safe_total = max(1, total_pages)
    safe_current = max(1, min(current_page, safe_total))
    safe_max_pages = max(1, c_max_pages)

    half = safe_max_pages // 2
    start = max(1, safe_current - half)
    end = min(safe_total, start + safe_max_pages - 1)
    if end == safe_total:
        start = max(1, end - safe_max_pages + 1)

    def href_for(page: int) -> str:
        return _page_href(
            c_base_url,
            page,
            page_param=page_param,
            query_params=query_params,
        )

    def link_attrs(href: str) -> dict[str, Any]:
        return _htmx_attrs(
            href,
            htmx=htmx,
            hx_target=hx_target,
            hx_swap=hx_swap,
            hx_push_url=hx_push_url,
        )

    links: list[Any] = []

    if c_show_first_last and safe_current > 1:
        href = href_for(1)
        links.append(
            Li(
                A("<<", href=href, cls="page-link", aria_label="First", **link_attrs(href)),
                cls="page-item",
            )
        )

    if c_show_prev_next:
        prev_disabled = safe_current == 1
        prev_page = max(1, safe_current - 1)
        href = href_for(prev_page)
        links.append(
            Li(
                (
                    A(
                        "<",
                        href=href,
                        cls="page-link",
                        aria_label="Previous",
                        **link_attrs(href),
                    )
                    if not prev_disabled
                    else Span("<", cls="page-link", aria_hidden="true")
                ),
                cls="page-item" + (" disabled" if prev_disabled else ""),
            )
        )

    for page in range(start, end + 1):
        active = page == safe_current
        href = href_for(page)
        links.append(
            Li(
                (
                    A(str(page), href=href, cls="page-link", **link_attrs(href))
                    if not active
                    else Span(str(page), cls="page-link")
                ),
                cls="page-item" + (" active" if active else ""),
                aria_current="page" if active else None,
            )
        )

    if c_show_prev_next:
        next_disabled = safe_current == safe_total
        next_page = min(safe_total, safe_current + 1)
        href = href_for(next_page)
        links.append(
            Li(
                (
                    A(">", href=href, cls="page-link", aria_label="Next", **link_attrs(href))
                    if not next_disabled
                    else Span(">", cls="page-link", aria_hidden="true")
                ),
                cls="page-item" + (" disabled" if next_disabled else ""),
            )
        )

    if c_show_first_last and safe_current < safe_total:
        href = href_for(safe_total)
        links.append(
            Li(
                A(">>", href=href, cls="page-link", aria_label="Last", **link_attrs(href)),
                cls="page-item",
            )
        )

    ul = Ul(*links, cls=ul_cls)

    nav_attrs: dict[str, Any] = {"aria_label": "Page navigation"}
    if hx_target:
        nav_attrs["hx_target"] = hx_target
    nav_attrs.update(convert_attrs(kwargs))

    return Nav(ul, cls=justify_class, **nav_attrs)