UIDex

Pagination

ترقيم الصفحات

<nav aria-label="pagination">

also calledpager، paging، page navigation، paginator، page numbers، dot indicator، باچينيشن، باجينيشن، شريط الصفحات، أرقام الصفحات، التنقّل بين الصفحات، مؤشّر النقاط

Pagination is a navigation region that cuts one large collection (search results, an orders table, an article archive) into equal slices and gives every slice an address of its own. That address is the payoff: page 7 has a URL like ?page=7 you can paste to a colleague, bookmark, let a crawler index, and come back next week to find the same position. It is not a stepper: steps are an ordered journey with a finish line where the task completes, while pages are interchangeable slices you jump between in any order with nothing to complete at the end. Infinite scroll is not the same control, because it throws addressability away altogether: no number, no link, and no route back to something you saw yesterday except scrolling from the top again. A "load more" button differs mechanically rather than stylistically: it appends the new batch under the old one so the DOM grows without a ceiling, while pagination replaces the content outright and the page stays the same size however deep the user goes. The practical rule: if a user will ever need to say "I am on page 4" or return to it, pagination is the only one of the three that can deliver it.

If you called it…

"the numbers at the bottom of the table""the 1 2 3 next row under search results""the previous and next arrows for a long list""the little dots under a slider""the thing that takes me to page 2"

Live specimen

Interact with the demo. Every part is real and numbered.

عرض ١٢١‎–‎١٤٠ من ٣٤٩صفحة ٧ من ١٨
نسخة الكاروسيل
RTLالصفحة ١ في اليمين، والسهمان ينعكسان، والأرقام لا تنعكس

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build pagination for a long results list. Make the page number the single source of truth and read it from the URL (?page=) rather than component state, so the browser back button works and the link is shareable. Wrap everything in a <nav aria-label> with a unique name per paginator on the page, holding a <ul> of items. Every number is a real <a href>, never a <button onClick>, and the current page is one element carrying aria-current="page", not a link, distinguished by something other than colour. Add previous and next controls with accessible names and rel="prev" / rel="next"; at a boundary drop the href and render <span aria-disabled="true"> instead of only lowering the opacity. Compute a truncation window with a fixed number of slots: the first page, the last page, and one neighbour either side of the current page. Put an aria-hidden="true" ellipsis between the ranges and space it with margin-inline. Use logical properties throughout: gap, padding-inline, margin-inline, inset-inline-start, and never margin-left or justify-content: left. Mirror the two chevron icons with scaleX(-1) under [dir="rtl"] and never reorder them in the DOM. If you add arrow-key navigation, resolve the direction with :dir(rtl) and invert the step instead of binding ArrowRight to the next page. Generate every numeral label from a single Intl.NumberFormat while keeping the value in the URL Latin, put an LRM (U+200E) on each side of the dash in any range such as "١٢١–١٤٠" because isolation alone will not stop the bidi algorithm flipping it to "١٤٠–١٢١" once the digits are Arabic-Indic, and add font-variant-numeric: tabular-nums so the row does not resize between 9 and 10. After each move, return focus to the top of the list and announce the new position in an aria-live="polite" region. When a page outside the range is requested, render an explicit empty state instead of a silent empty table.

How it behaves right-to-left

This section is ours alone.

The order of the numbers

mirrors

The whole row mirrors: page 1 sits at the far right, the last page at the left, and the "next" arrow ends up on the left edge. A flex-direction: row container does this by itself, because the inline axis follows direction. Never add row-reverse under [dir="rtl"]: it double-flips and drops page 1 back on the left. Alignment has a similar trap: justify-content: flex-start is a logical value that follows direction, while justify-content: left is physical and hugs the left edge in both languages.

The prev and next chevrons

mirrors

These two are directional in the literal sense: "previous" must point right in Arabic and "next" must point left, because they point at where the page sits on screen, not at an abstract idea. An SVG knows nothing about bidi, so flip it explicitly with [dir="rtl"] .chevron { transform: scaleX(-1) }. The opposite mistake is worse. Never swap the two buttons in the markup: direction already does the visual flip, and reordering the DOM flips them a second time, putting them back where they started while making Tab reach "next" before "previous".

Numerals and the character between them

never mirrors

The digits themselves never mirror, but the neutral character between two of them does: inside an Arabic paragraph the dash in "١٢١–١٤٠" takes the direction of its surroundings and the range renders as "١٤٠–١٢١", so a "showing … of 349" line reads back a reversed count. <bdi> only rescues Latin digits here، rule W7 retypes them as strong inside an LTR isolate، while Arabic-Indic digits are bidi class AN, which isolation does not change, so the range stays flipped; put an LRM (U+200E) on each side of the dash, or unicode-bidi: bidi-override with direction: ltr on the range alone, or write "من ١٢١ إلى ١٤٠" and be rid of the neutral entirely. Pick one numeral system for the whole region, Arabic-Indic ١ ٢ ٣ or Latin 1 2 3, and never mix them as in "صفحة ٣ من 18". The value in the URL always stays Latin: ?page=٣ is not a number to your server and Number("٣") returns NaN, so format the visible label with Intl.NumberFormat("ar-EG") and send the value untouched.

The arrow keys

mirrors

A keydown event hands you a physical key: ArrowRight stays ArrowRight whatever the page direction, and the browser will not mirror it for you. But an Arabic reader expects the right arrow to move to the previous page and the left arrow to the next, the exact opposite of English. WAI-ARIA requires that of every composite widget in RTL. Read the direction off the element with el.matches(":dir(rtl)") or getComputedStyle(el).direction, multiply the step by -1 under rtl, and never wire "next page" straight to ArrowRight in the handler.

Carousel dots and scrolling

mirrors

The dot row mirrors just like the numbers: the dot for the first slide sits at the right. The layout is fine; the arithmetic is what breaks: in an RTL scroll container scrollLeft starts at 0 at the rightmost position and goes negative as you advance, so Math.round(el.scrollLeft / slideWidth) yields a negative index and lights the wrong dot; the correct form is Math.round(Math.abs(el.scrollLeft) / slideWidth). To move, use slide.scrollIntoView({ inline: "start" }) or scroll-snap-align: start, both logical and direction-aware, unlike a track driven by translateX(-100% * i), whose sign has to be inverted under RTL.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIAaria-current="page"The one marker that names the active page. Only one element ever carries it.
HTML<nav aria-label> + <ul>A named navigation landmark around a list; <ul> is enough because the visible numbers carry the order.
HTMLrel="prev" / rel="next"Valid link relations; Google stopped using them for indexing in 2019, so they signal to other agents rather than move SEO.
shadcn/ui<Pagination><PaginationContent>…Compound parts: PaginationItem, PaginationLink with isActive setting aria-current, and PaginationEllipsis.
Material UI<Pagination count={18} siblingCount={1} />The truncation window is configured with siblingCount and boundaryCount; renderItem is where you inject a real href.
Ant Design<Pagination total={349} showSizeChanger />Adds a page-size selector and a total counter. Most hand-rolled versions forget both.
CSSmargin-inlineThe logical spacing around the ellipsis; it mirrors itself instead of a hard-coded margin-left.

See also

Updated · 2026-08-19