View as Markdown

Pagination

Pagination with page navigation, next and previous links.

Loading...
<twig:Pagination>
    <twig:Pagination:Content>
        <twig:Pagination:Item>
            <twig:Pagination:Previous href="#" />
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">1</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#" active>2</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">3</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Ellipsis />
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Next href="#" />
        </twig:Pagination:Item>
    </twig:Pagination:Content>
</twig:Pagination>

Installation

php bin/console ux:install pagination --kit shadcn

Install the following Composer dependencies:

composer require symfony/ux-icons tales-from-a-dev/twig-tailwind-extra:^1.0.0 twig/extra-bundle twig/html-extra:^3.12.0

Copy the following file(s) into your app:

{# @block content The pagination structure, typically a `Pagination:Content`. #}
&lt;nav
    data-slot=&quot;pagination&quot;
    aria-label=&quot;pagination&quot;
    role=&quot;navigation&quot;
    class=&quot;{{ (&#039;mx-auto flex w-full justify-center &#039; ~ attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ attributes.without(&#039;class&#039;) }}
&gt;
    {%- block content %}{% endblock -%}
&lt;/nav&gt;
{# @block content The pagination items, typically multiple `Pagination:Item` components. #}
&lt;ul
    data-slot=&quot;pagination-content&quot;
    class=&quot;{{ (&#039;flex items-center gap-0.5 &#039; ~ attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ attributes.without(&#039;class&#039;) }}
&gt;
    {%- block content %}{% endblock -%}
&lt;/ul&gt;
&lt;span
    aria-hidden=&quot;true&quot;
    data-slot=&quot;pagination-ellipsis&quot;
    class=&quot;{{ (&#039;flex size-8 items-center justify-center [&amp;_svg:not([class*=\&#039;size-\&#039;])]:size-4 &#039; ~ attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ attributes.without(&#039;class&#039;) }}
&gt;
    &lt;twig:ux:icon name=&quot;lucide:more-horizontal&quot; /&gt;
    &lt;span class=&quot;sr-only&quot;&gt;More pages&lt;/span&gt;
&lt;/span&gt;
{# @block content The pagination item content, typically a `Pagination:Link`. #}
&lt;li data-slot=&quot;pagination-item&quot; {{ attributes }}&gt;
    {%- block content %}{% endblock -%}
&lt;/li&gt;
{# @prop active boolean Whether this is the current page. #}
{# @block content The page number or navigation icon. #}
{%- props active = false -%}
&lt;twig:Button
    as=&quot;a&quot;
    variant=&quot;{{ active ? &#039;outline&#039; : &#039;ghost&#039; }}&quot;
    size=&quot;icon&quot;
    aria-current=&quot;{{ active ? &#039;page&#039; : false }}&quot;
    data-slot=&quot;pagination-link&quot;
    data-active=&quot;{{ active ? &#039;true&#039; : &#039;false&#039; }}&quot;
    {{ ...attributes }}
&gt;
    {{- block(outerBlocks.content) -}}
&lt;/twig:Button&gt;
{# @prop text string The label text. #}
{%- props text = &#039;Next&#039; -%}
&lt;twig:Pagination:Link
    aria-label=&quot;Go to next page&quot;
    size=&quot;default&quot;
    class=&quot;{{ (&#039;ltr:pr-1.5! rtl:pe-1.5! &#039; ~ attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ ...attributes.without(&#039;class&#039;) }}
&gt;
    &lt;span class=&quot;hidden sm:block&quot;&gt;{{ text }}&lt;/span&gt;
    &lt;twig:ux:icon name=&quot;lucide:chevron-right&quot; data-icon=&quot;inline-end&quot; class=&quot;rtl:rotate-180&quot; /&gt;
&lt;/twig:Pagination:Link&gt;
{# @prop text string The label text. #}
{%- props text = &#039;Previous&#039; -%}
&lt;twig:Pagination:Link
    aria-label=&quot;Go to previous page&quot;
    size=&quot;default&quot;
    class=&quot;{{ (&#039;ltr:pl-1.5! rtl:ps-1.5! &#039; ~ attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ ...attributes.without(&#039;class&#039;) }}
&gt;
    &lt;twig:ux:icon name=&quot;lucide:chevron-left&quot; data-icon=&quot;inline-start&quot; class=&quot;rtl:rotate-180&quot; /&gt;
    &lt;span class=&quot;hidden sm:block&quot;&gt;{{ text }}&lt;/span&gt;
&lt;/twig:Pagination:Link&gt;
{# @prop variant &#039;default&#039;|&#039;secondary&#039;|&#039;destructive&#039;|&#039;outline&#039;|&#039;ghost&#039;|&#039;link&#039; The visual style variant. #}
{# @prop size &#039;default&#039;|&#039;xs&#039;|&#039;sm&#039;|&#039;lg&#039;|&#039;icon&#039;|&#039;icon-xs&#039;|&#039;icon-sm&#039;|&#039;icon-lg&#039; The button size. #}
{# @prop as &#039;button&#039; The HTML tag to render. #}
{# @block content The button label and/or icon. #}
{%- props variant = &#039;default&#039;, size = &#039;default&#039;, as = &#039;button&#039; -%}
{%- set style = html_cva(
    base: &quot;group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 [&amp;_svg:not([class*=&#039;size-&#039;])]:size-4&quot;,
    variants: {
        variant: {
            default: &#039;bg-primary text-primary-foreground [a]:hover:bg-primary/80&#039;,
            outline: &#039;border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50&#039;,
            secondary: &#039;bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground&#039;,
            ghost: &#039;hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50&#039;,
            destructive: &#039;bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40&#039;,
            link: &#039;text-primary underline-offset-4 hover:underline&#039;,
        },
        size: {
            default: &#039;h-8 gap-1.5 px-2.5 ltr:has-data-[icon=inline-end]:pr-2 rtl:has-data-[icon=inline-end]:pe-2 ltr:has-data-[icon=inline-start]:pl-2 rtl:has-data-[icon=inline-start]:ps-2&#039;,
            xs: &quot;h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg ltr:has-data-[icon=inline-end]:pr-1.5 rtl:has-data-[icon=inline-end]:pe-1.5 ltr:has-data-[icon=inline-start]:pl-1.5 rtl:has-data-[icon=inline-start]:ps-1.5 [&amp;_svg:not([class*=&#039;size-&#039;])]:size-3&quot;,
            sm: &quot;h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg ltr:has-data-[icon=inline-end]:pr-1.5 rtl:has-data-[icon=inline-end]:pe-1.5 ltr:has-data-[icon=inline-start]:pl-1.5 rtl:has-data-[icon=inline-start]:ps-1.5 [&amp;_svg:not([class*=&#039;size-&#039;])]:size-3.5&quot;,
            lg: &#039;h-9 gap-1.5 px-2.5 ltr:has-data-[icon=inline-end]:pr-2 rtl:has-data-[icon=inline-end]:pe-2 ltr:has-data-[icon=inline-start]:pl-2 rtl:has-data-[icon=inline-start]:ps-2&#039;,
            icon: &#039;size-8&#039;,
            &#039;icon-xs&#039;: &quot;size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&amp;_svg:not([class*=&#039;size-&#039;])]:size-3&quot;,
            &#039;icon-sm&#039;: &#039;size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg&#039;,
            &#039;icon-lg&#039;: &#039;size-9&#039;,
        },
    },
) -%}
&lt;{{ as }}
    data-slot=&quot;button&quot;
    data-variant=&quot;{{ variant }}&quot;
    data-size=&quot;{{ size }}&quot;
    class=&quot;{{ style.apply({variant: variant, size: size}, attributes.render(&#039;class&#039;))|tailwind_merge }}&quot;
    {{ attributes }}
&gt;
    {%- block content %}{% endblock -%}
&lt;/{{ as }}&gt;

Usage

<twig:Pagination>
    <twig:Pagination:Content>
        <twig:Pagination:Item>
            <twig:Pagination:Previous href="#" />
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">1</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#" active>2</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">3</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Ellipsis />
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Next href="#" />
        </twig:Pagination:Item>
    </twig:Pagination:Content>
</twig:Pagination>

Examples

Simple

A simple pagination with only page numbers.

Loading...
<twig:Pagination>
    <twig:Pagination:Content>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">1</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#" active>2</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">3</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">4</twig:Pagination:Link>
        </twig:Pagination:Item>
        <twig:Pagination:Item>
            <twig:Pagination:Link href="#">5</twig:Pagination:Link>
        </twig:Pagination:Item>
    </twig:Pagination:Content>
</twig:Pagination>

Icons Only

Use just the previous and next buttons without page numbers. This is useful for data tables with a rows per page selector.

Loading...
<div class="flex items-center justify-between gap-4">
    <twig:Field orientation="horizontal" class="w-fit">
        <twig:Field:Label for="select-rows-per-page">Rows per page</twig:Field:Label>
        <twig:Select id="select-rows-per-page" class="w-20">
            <option value="10">10</option>
            <option value="25" selected>25</option>
            <option value="50">50</option>
            <option value="100">100</option>
        </twig:Select>
    </twig:Field>
    <twig:Pagination class="mx-0 w-auto">
        <twig:Pagination:Content>
            <twig:Pagination:Item>
                <twig:Pagination:Previous href="#" />
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Next href="#" />
            </twig:Pagination:Item>
        </twig:Pagination:Content>
    </twig:Pagination>
</div>

RTL

To enable RTL support, set the dir="rtl" attribute on the root element.

Loading...
<div class="flex flex-col gap-8">
    {# Arabic #}
    <twig:Pagination dir="rtl">
        <twig:Pagination:Content>
            <twig:Pagination:Item>
                <twig:Pagination:Previous href="#" text="السابق" />
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Link href="#">١</twig:Pagination:Link>
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Link href="#" active>٢</twig:Pagination:Link>
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Link href="#">٣</twig:Pagination:Link>
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Ellipsis />
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Next href="#" text="التالي" />
            </twig:Pagination:Item>
        </twig:Pagination:Content>
    </twig:Pagination>

    {# Hebrew #}
    <twig:Pagination dir="rtl">
        <twig:Pagination:Content>
            <twig:Pagination:Item>
                <twig:Pagination:Previous href="#" text="הקודם" />
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Link href="#">1</twig:Pagination:Link>
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Link href="#" active>2</twig:Pagination:Link>
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Link href="#">3</twig:Pagination:Link>
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Ellipsis />
            </twig:Pagination:Item>
            <twig:Pagination:Item>
                <twig:Pagination:Next href="#" text="הבא" />
            </twig:Pagination:Item>
        </twig:Pagination:Content>
    </twig:Pagination>
</div>

API Reference

<twig:Pagination>

Block Description
content The pagination structure, typically a Pagination:Content.

<twig:Pagination:Content>

Block Description
content The pagination items, typically multiple Pagination:Item components.

<twig:Pagination:Item>

Block Description
content The pagination item content, typically a Pagination:Link.
Prop Type Default
active 
boolean false
Block Description
content The page number or navigation icon.

<twig:Pagination:Next>

Prop Type Default
text 
string 'Next'

<twig:Pagination:Previous>

Prop Type Default
text 
string 'Previous'