Tucano v0.37.2

Pagination

Made for Django's Paginator: you pass the page number and the total, and it builds the rest. The items are links with a real href that keep the filter, search and ordering from the URL — so opening in another tab, the browser's back button and search engines keep working.

Examples

On this page clicks don't navigate: each example swaps the current page in place, so you can try it out.

Few pages

No ellipsis, and the inactive edge gets out of the way.

Wider neighborhood

data-around="2" shows two pages on each side of the current one.

More pages at the edges

data-edges="2" keeps the first two and the last two.

Other labels and parameter

data-prev-text, data-next-text and data-param="p" in the URL.

Centered

class="tuc-pagination is-center" alongside data-tuc-pagination.

Right-aligned

is-end, to sit under a table's amount column.

How to use

With the numbers the Paginator already has. The element is replaced by the navigation on load and on every htmx:afterSwap; with a single page, nothing is rendered — there's nowhere to go.

In the Django template

{% for contract in page_obj %}
  …
{% endfor %}

<div data-tuc-pagination
     data-page="{{ page_obj.number }}"
     data-pages="{{ page_obj.paginator.num_pages }}"></div>
from django.core.paginator import Paginator

def contracts(request):
    qs = Contract.objects.filter(customer__name__icontains=request.GET.get("q", "")).order_by("-created_at")
    page_obj = Paginator(qs, 25).get_page(request.GET.get("page"))
    return render(request, "contracts/list.html", {"page_obj": page_obj})

On a page at /contracts/?q=padaria&sort=amount, the link to page 3 comes out as /contracts/?q=padaria&sort=amount&page=3: only the page parameter changes. That's what keeps the search and the ordering standing when you turn the page, without the template having to rebuild the query string.

AttributeDefaultWhat for
data-page1The current page
data-pages1The total number of pages
data-parampageName of the parameter in the query string
data-around1Visible pages on each side of the current one
data-edges1Visible pages at each edge
data-prev-textAnteriorLabel of the back edge
data-next-textPróximaLabel of the forward edge

With HTMX

Since the items are already links, hx-boost on a container above is enough to turn page changes into partial requests — and the pagination that comes back in the new fragment initializes on its own. No onChange needed.

In JavaScript

For those who build the list on the client. The Tucano.pagination() shortcut returns the element ready to insert; the class returns the instance, with the element in node.

const pag = new Tucano.Pagination({
  page: 1,
  pages: 12,
  label: 'Contracts pagination',      // aria-label of the navigation
  onChange: (page, p) => {            // with onChange, clicking doesn't navigate
    load(page);
    p.setPage(page);                  // redraws with the new current page
  },
});
document.querySelector('#footer').append(pag.node);

pag.href(3);          // '/contracts/?q=padaria&page=3' — the link to a page
pag.opts.pages = 15;
pag.render();         // redraws after changing the options
pag.destroy();        // removes the element

// Just the element, without keeping the instance
document.querySelector('#footer').append(Tucano.pagination({ page: 2, pages: 5 }));

Alignment is a class on .tuc-pagination: from JavaScript, pag.node.classList.add('is-center'), or is-end. In the template the marked element receives the navigation inside it, so write the class next to the attribute:

<div class="tuc-pagination is-center" data-tuc-pagination
     data-page="{{ page_obj.number }}"
     data-pages="{{ page_obj.paginator.num_pages }}"></div>

Which numbers show up

With many pages, not everything fits. The edges, the current page and its neighbors stay; the rest becomes an ellipsis. The calculation is exported as Tucano.pageWindow(), and returns null where the ellipsis goes:

Tucano.pageWindow(5, 20);                 // [1, null, 4, 5, 6, null, 20]
Tucano.pageWindow(3, 7);                  // [1, 2, 3, 4, null, 7]
Tucano.pageWindow(10, 30, { around: 2 }); // [1, null, 8, 9, 10, 11, 12, null, 30]

A gap of a single page never becomes an ellipsis: showing the number takes the same space and gives one more place to click. That's why, on page 3 of 7, the 2 shows up instead of ….

In a server-paginated list — the case of every Django CRUD — the page number lives in the URL. Swapping the links for onclick would break the middle-click, the browser's back button and indexing all at once.

The items are the system's button

Each item is a .tuc-btn is-ghost, and the current page is is-outline. Height, radius, focus and touch are already solved in the button, and a second button design on the same screen is what gives away a patched-together library. The current page gets an outline, not a fill: in a bar with ten targets, ten solid buttons would fight each other.

The disabled edge is a <span>, not an <a> without href

A link that leads nowhere stays in the Tab path and is announced as a link. The <span> with aria-disabled simply gets out of the way, and .tuc-btn draws the disabled state without an extra rule.

On mobile (below 40rem) the words at the edges go away and the arrows stay: the space is worth more for the numbers. Before the script builds it, the empty element already reserves the height of a control, so the content below doesn't jump up.

Keyboard and accessibility

They're links in a <nav>, and the keyboard is the browser's.

KeyAction
Tab Shift+TabMoves between pages; the disabled edge and the ellipsis stay out of the path
EnterGoes to the focused page

The navigation is role="navigation" with aria-label — "Paginação" by default, which you can change with the label option when there are two on the same screen. The current page carries aria-current="page", and the ellipsis, aria-hidden.

API

Generated from the code on every build — if something is not here, it does not exist.

Markup
[data-tuc-pagination]
In JS
new Tucano.Pagination(opcoes) Tucano.pageWindow() Tucano.pagination()
Attributes
data-around data-edges data-next-text data-page data-pages data-param data-prev-text
Methods
href render setPage destroy

Options

The notes in this table come from comments in the source code, which are written in Portuguese.

OptionDefaultWhat for
page1
pages1
param'page'
around1paginas visiveis de cada lado da atual
edges1paginas visiveis nas pontas
prevTextundefineddefault: setTexts ("Anterior")
nextTextundefineddefault: setTexts ("Próxima")
labelundefineddefault: setTexts ("Paginação")
onChangenull