Symfony UX Pagination

Pagination for all.
One page ahead.

Paginate any data source. Choose pages, lookahead, or cursors, then render with Twig and add LiveComponent without giving up real URLs.

composer require symfony/ux-pagination

What UX Pagination handles

  • Any source

    Doctrine ORM or DBAL, arrays, callbacks, and custom adapters.

  • Pages or cursors

    Numbered pages, count-free lookahead, or stable cursor navigation.

  • Symfony native

    Request validation, Router links, DI, and named configuration.

  • Made for Twig

    Accessible server-rendered links, themes, and TwigComponent.

  • Live-ready

    LiveComponent and Turbo updates that keep real URLs.

ONE FLOW, END TO END.

From your query to pagination links.

Give PaginatorInterface an unpaginated source. It resolves the current request, fetches one slice, and returns an iterable result ready for Twig.

src/Controller/ProductController.php
public function __invoke(
    ProductRepository $repository,
    PaginatorInterface $paginator,
): Response {
    $products = $paginator->paginate(
        $repository->createListQuery(),
    );

    return $this->render('product/index.html.twig', [
        'products' => $products,
    ]);
}
templates/product/index.html.twig
{% for product in products %}
    <article>
        <h3>{{ product.name }}</h3>
        <p>{{ product.category }}</p>
        <strong>{{ product.price }}</strong>
    </article>
{% endfor %}

{{ ux_pagination(products) }}

Bring any data source.

UX Pagination adapts what your application already returns. Your repositories and API clients stay focused on data; the paginator owns page state, limits, and navigation.

  • Doctrine ORM
  • Doctrine DBAL
  • Arrays
  • APIs and search
One contract PaginatorInterface

One iterable result for controllers, Twig, and LiveComponent.

Choose the right pagination strategy.

The interface stays the same. Choose the navigation promise your screen actually needs.

Numbered pages

Use page numbers when people need exact totals and direct access to any page.

Before Page 4
Now Page 5
After Page 6
Total
Known
Jump to page
Yes
Count query
Yes

Lookahead

Fetch one extra item to know whether a next page exists, without running a count.

Before Already seen
Now Current slice
After One-item peek
Total
Unknown
Next page
Known
Count query
No

Cursor

Continue from a signed position when rows can change while someone is browsing.

Before Before cursor
Now Current slice
After Continues
Total
Unknown
Position
Stable
Changing data
Safe
Choose in PHP numbered ->paginate() lookahead ->lookahead()->paginate() cursor ->cursor($query)->paginate()

YOUR MARKUP, YOUR RULES.

Keep pagination à la page.

Keep the default markup, choose Bootstrap or Tailwind, or replace the template. Pagination state and URLs stay outside the theme.

Default

Accessible markup, ready for your CSS.

Bootstrap

Familiar Bootstrap pagination classes.

Tailwind

Utility-ready markup for Tailwind.

Change the theme, not the pagination logic.

{{ ux_pagination(products) }}

{{ ux_pagination(products, theme: '@UXPagination/theme/tailwind.html.twig') }}
Live Pagination demo with jersey filters and pagination controls
Live Pagination demo preview

Live updates. Keep the URL.

Add the trait when filters or page changes should update in place. Every control keeps a real href, so the same screen remains linkable and works without JavaScript.

src/Twig/Components/ProductList.php
#[AsLiveComponent]
final class ProductList
{
    use ComponentWithPaginationTrait;

    protected function createPagination(): PaginationBuilder
    {
        return $this->paginator->query(
            $this->products->searchQuery(),
        );
    }
}
See the live demo

Built for a real Symfony application.

Keep the convenient defaults, then make configuration, dependency injection, and tests explicit where your application needs control.

Configure by context

Set shared defaults once, then add a named profile when one list needs a different pagination policy.

ux_pagination:
    items_per_page: 20
    paginators:
        catalog:
            items_per_page: 24
            navigation: { size: 7 }

Inject with #[Target]

Select a named paginator explicitly while keeping the controller property focused on its application role.

final class CatalogController
{
    public function __construct(
        #[Target('catalog')]
        private PaginatorInterface $paginator,
    ) {}
}

Test the real contract

Build the real paginator with deterministic request, URL, and cursor behavior without booting the application.

$paginator = PaginatorFactory::create(
    defaultPerPage: 10,
);
$page = $paginator->paginate(
    range(1, 25), page: 2,
);
self::assertSame(
    range(11, 20), $page->getItems(),
);

Install It

$ composer require symfony/ux-pagination

See every page in action.

Browse pagination demos
Live Pagination

Live Pagination

Add reactive filters and page changes to ordinary pagination links.

Cursor Pagination

Cursor Pagination

Move through ordered values without letting earlier changes shift the current slice.

Pagination Themes

Pagination Themes

Render the same pages with a built-in theme or a focused Twig block override.