Skip to content

Pagination

The Pagination component creates page navigation controls for browsing through large datasets. Essential for tables, search results, and any content split across multiple pages.

Goal

Master creating pagination controls, understand Bootstrap pagination classes, and build intuitive page navigation that helps users explore content efficiently.


Quick Start

Live Preview
from faststrap import Pagination

Pagination(
    current_page=2,
    total_pages=10
)

Visual Examples & Use Cases

1. Sizes - Match Your Design

Live Preview
# Large - prominent pagination
Pagination(current_page=2, total_pages=10, size="lg")

# Default - standard use
Pagination(current_page=2, total_pages=10)

# Small - compact tables
Pagination(current_page=2, total_pages=10, size="sm")

2. Alignment - Position Controls

Live Preview
# Left aligned (default)
Pagination(current_page=2, total_pages=10, align="start")

# Center aligned
Pagination(current_page=2, total_pages=10, align="center")

# Right aligned
Pagination(current_page=2, total_pages=10, align="end")

3. First/Last Buttons - Quick Navigation

Live Preview
Pagination(
    current_page=2,
    total_pages=100,
    show_first_last=True,  # ← Adds « and » buttons
    show_prev_next=True    # ← Adds ‹ and › buttons (default)
)

Practical Functionality

HTMX Integration - Dynamic Page Loading

Load pages dynamically without full page refresh.

from faststrap import Pagination, Table, Card

@app.get("/")
def home():
    return Card(
        Div(id="data-table", hx_get="/page/1", hx_trigger="load"),
        Div(id="pagination-controls"),
        header="Product Catalog"
    )

@app.get("/page/{page}")
def load_page(page: int):
    # Get data for this page
    products = get_products(page=page, per_page=20)
    total_pages = get_total_pages(per_page=20)

    # Return table and pagination
    return Div(
        Table(
            THead(Tr(Th("Name"), Th("Price"), Th("Stock"))),
            TBody(*[
                Tr(Td(p.name), Td(f"${p.price}"), Td(p.stock))
                for p in products
            ]),
            id="data-table"
        ),
        Pagination(
            current_page=page,
            total_pages=total_pages,
            base_url="/page",
            align="center",
            hx_get=True,  # Use HTMX for page clicks
            hx_target="#data-table",
            hx_swap="outerHTML",
            id="pagination-controls"
        )
    )

Search Results Pagination

@app.get("/search")
def search(q: str, page: int = 1):
    results = search_database(q, page=page, per_page=10)
    total_pages = calculate_total_pages(q, per_page=10)

    return Div(
        H4(f"Search results for '{q}'"),
        Div(*[result_card(r) for r in results]),
        Pagination(
            current_page=page,
            total_pages=total_pages,
            base_url=f"/search?q={q}",
            align="center",
            max_pages=7  # Show max 7 page numbers
        ),
        cls="container my-4"
    )

Infinite Scroll Alternative

Provide pagination as fallback for infinite scroll.

@app.get("/feed")
def feed(page: int = 1):
    posts = get_posts(page=page, per_page=20)
    total_pages = get_total_pages(per_page=20)

    return Div(
        # Posts
        Div(
            *[post_card(p) for p in posts],
            id="posts-container"
        ),

        # Pagination (fallback if JS disabled)
        Pagination(
            current_page=page,
            total_pages=total_pages,
            base_url="/feed",
            align="center",
            cls="mt-4"
        ),

        # Infinite scroll trigger (if JS enabled)
        Div(
            id="load-more-trigger",
            hx_get=f"/feed?page={page+1}",
            hx_trigger="revealed",
            hx_swap="beforebegin",
            hx_target="#posts-container"
        ) if page < total_pages else None
    )

Bootstrap CSS Classes Explained

Core Pagination Classes

Class Purpose Applied To
.pagination Container - Styles the list <ul> element
.pagination-lg Large size - Bigger buttons <ul> element
.pagination-sm Small size - Compact buttons <ul> element
.page-item Item wrapper - Wraps each link <li> elements
.page-link Link styling - Styles the clickable area <a> or <span>
.page-item.active Active state - Current page <li> element
.page-item.disabled Disabled state - Non-clickable <li> element

Alignment Classes

Class Purpose Effect
.justify-content-start Left align - Default position Aligns to left
.justify-content-center Center align - Center position Centers pagination
.justify-content-end Right align - Right position Aligns to right

State Classes

Class Purpose Visual Effect
.active Current page Blue background, white text
.disabled Non-clickable item Grayed out, no hover

Responsive Pagination Patterns

Mobile-Friendly Pagination

from faststrap import Pagination

# Show fewer pages on mobile
def ResponsivePagination(current: int, total: int):
    return Div(
        # Desktop - show more pages
        Pagination(
            current_page=current,
            total_pages=total,
            max_pages=7,
            show_first_last=True,
            cls="d-none d-md-flex"
        ),
        # Mobile - show fewer pages
        Pagination(
            current_page=current,
            total_pages=total,
            max_pages=3,
            show_first_last=False,
            size="sm",
            cls="d-md-none"
        )
    )

Core Faststrap Features

Global Defaults with set_component_defaults

from faststrap import set_component_defaults, Pagination

# All pagination centered with first/last buttons
set_component_defaults("Pagination", 
                      align="center", 
                      show_first_last=True,
                      max_pages=7)

# Now all pagination inherits these defaults
Pagination(current_page=5, total_pages=20)
# ↑ Automatically centered with first/last buttons

# Override when needed
Pagination(current_page=1, total_pages=5, align="start", show_first_last=False)

Common Default Patterns:

# Data tables - compact pagination
set_component_defaults("Pagination", size="sm", align="end")

# Search results - centered pagination
set_component_defaults("Pagination", align="center", max_pages=9)

# Admin panels - large, prominent pagination
set_component_defaults("Pagination", size="lg", show_first_last=True)

Common Recipes

The "Page Info" Pattern

Show current page info alongside pagination.

def PaginationWithInfo(current: int, total: int, total_items: int):
    start = (current - 1) * 20 + 1
    end = min(current * 20, total_items)

    return Div(
        Div(
            f"Showing {start}-{end} of {total_items} items",
            cls="text-muted small mb-2"
        ),
        Pagination(
            current_page=current,
            total_pages=total,
            align="center"
        )
    )

The "Per Page Selector" Pattern

Let users choose items per page.

from faststrap import Select, Pagination

def PaginationWithPerPage(current: int, total_items: int, per_page: int = 20):
    total_pages = (total_items + per_page - 1) // per_page

    return Div(
        Div(
            Select(
                "per_page",
                ("10", "10 per page"),
                ("20", "20 per page", per_page == 20),
                ("50", "50 per page"),
                ("100", "100 per page"),
                hx_get="/update-pagination",
                hx_target="#pagination-container",
                cls="form-select-sm"
            ),
            Pagination(
                current_page=current,
                total_pages=total_pages,
                align="center"
            ),
            cls="d-flex justify-content-between align-items-center"
        ),
        id="pagination-container"
    )

Accessibility Best Practices

Faststrap automatically handles accessibility:

Automatic Features: - aria-label="Page navigation" on <nav> - aria-current="page" on active page - aria-label on first/last/prev/next buttons - Semantic <nav> structure

Manual Enhancements:

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

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 Pagination size
align "start" \| "center" \| "end" "start" Alignment
max_pages int \| None 5 Maximum page numbers to show
base_url str \| None "#" Base URL for page links
show_first_last bool \| None False Show first/last page buttons
show_prev_next bool \| None True Show previous/next buttons
**kwargs Any - Additional HTML attributes (cls, id, hx-*)

faststrap.components.navigation.pagination.Pagination(current_page, total_pages, size=None, align=None, max_pages=None, base_url=None, show_first_last=None, show_prev_next=None, **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)

None
align AlignType | None

Alignment (start, center, end)

None
max_pages int | None

Maximum page numbers to show

None
base_url str | None

Base URL for page links

None
show_first_last bool | None

Show first/last page buttons

None
show_prev_next bool | None

Show previous/next buttons

None
**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 = None,
    align: AlignType | None = None,
    max_pages: int | None = None,
    base_url: str | None = None,
    show_first_last: bool | None = None,
    show_prev_next: bool | None = None,
    **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
        **kwargs: Additional HTML attributes
    """
    # Resolve API defaults
    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)

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

    # Alignment
    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)

    # Calculate page range
    half = c_max_pages // 2
    start = max(1, current_page - half)
    end = min(total_pages, start + c_max_pages - 1)

    # Adjust if at end
    if end == total_pages:
        start = max(1, end - c_max_pages + 1)

    # Build page links
    links: list[Any] = []

    # First page
    if c_show_first_last and current_page > 1:
        links.append(
            Li(
                A("«", href=f"{c_base_url}?page=1", cls="page-link", aria_label="First"),
                cls="page-item",
            )
        )

    # Previous page
    if c_show_prev_next:
        prev_disabled = current_page == 1
        prev_page = max(1, current_page - 1)
        links.append(
            Li(
                (
                    A(
                        "‹",
                        href=f"{c_base_url}?page={prev_page}",
                        cls="page-link",
                        aria_label="Previous",
                    )
                    if not prev_disabled
                    else Span("‹", cls="page-link", aria_hidden="true")
                ),
                cls="page-item" + (" disabled" if prev_disabled else ""),
            )
        )

    # Page numbers
    for page in range(start, end + 1):
        active = page == current_page
        href = f"{c_base_url}?page={page}"
        links.append(
            Li(
                (
                    A(str(page), href=href, cls="page-link")
                    if not active
                    else Span(str(page), cls="page-link")
                ),
                cls="page-item" + (" active" if active else ""),
                aria_current="page" if active else None,
            )
        )

    # Next page
    if c_show_prev_next:
        next_disabled = current_page == total_pages
        next_page = min(total_pages, current_page + 1)
        links.append(
            Li(
                (
                    A(
                        "›",
                        href=f"{c_base_url}?page={next_page}",
                        cls="page-link",
                        aria_label="Next",
                    )
                    if not next_disabled
                    else Span("›", cls="page-link", aria_hidden="true")
                ),
                cls="page-item" + (" disabled" if next_disabled else ""),
            )
        )

    # Last page
    if c_show_first_last and current_page < total_pages:
        links.append(
            Li(
                A("»", href=f"{c_base_url}?page={total_pages}", cls="page-link", aria_label="Last"),
                cls="page-item",
            )
        )

    # Build pagination
    ul = Ul(*links, cls=ul_cls)

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

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