Skip to content

RTL pagination: order, arrows and labels

Build RTL pagination with consistent page order, translated labels, directional arrows, localized digits and reliable first and last page states.

By , Founder of Ritla

Guide13 min read

A pagination control can look Arabic while sending “next” to the previous result set. That happens when page arithmetic is tied to the arrow's physical direction: the icon is mirrored, then the click handler is changed to subtract a page because the arrow now points left.

Keep the operation and its presentation separate. Next advances through the chosen result sequence. Previous returns through it. In a conventional horizontal RTL pager, next points left and previous points right, while page numbers progress from the reading start. Neither operation changes meaning when the language changes.

The examples use a community repair-workshop directory, currently on page six of eighteen. The visible page window contains five, six and seven. This matters when evaluating a screenshot: page one belongs at the start of a full ascending page list, but a windowed pager need not display page one at all.

Define next from the result sequence#

For numbered pagination, next means the current page index plus one. Previous means minus one. Those relationships are independent of whether results are sorted alphabetically, by availability or by newest listing. Changing the sort may change the records, but it does not turn page seven into the previous page after page six.

A fragile handler derives its behavior from geometry, such as treating a left-pointing control as “decrease.” Instead, give the action a semantic identity and calculate destinations from the pagination state:

js
function paginationState(page, totalPages) {  if (!Number.isInteger(page) || !Number.isInteger(totalPages) ||      totalPages < 1 || page < 1 || page > totalPages) {    throw new RangeError("Expected a page inside a nonempty result set");  }   return {    current: page,    previous: page > 1 ? page - 1 : null,    next: page < totalPages ? page + 1 : null  };}

Calling this with page six and eighteen total pages produces previous five and next seven in either locale. The renderer can then choose labels, arrows and layout direction without modifying those destinations.

The helper deliberately excludes empty results. An empty directory needs a separate state without a current result page. Do not force it through this function by inventing “page one of zero,” or leave navigation pointing into an empty sequence.

Keep the public page number separate from the API's indexing convention too. A service may call its first page index zero while the directory calls it page one. In that contract, visible page six maps to request index five in both languages. RTL is not another reason to subtract from the public page number.

For an offset-based endpoint with sixteen records per page, visible page six starts at offset eighty: (6 - 1) * 16. That calculation does not change when the pager moves to the other side of the directory. Capture the outgoing request when a control appears correct but loads the wrong records; an indexing mismatch can resemble a reversed next action.

Cursor pagination has a different destination model. Its next cursor is an opaque value returned by the service, not a number to increment or reverse. Keep the same separation: direction controls presentation, while the service defines which cursor advances the sequence. If the total is unknown, do not manufacture a last-page link or an exact page count.

Keep ascending page order in the source#

For the directory's page window, write the controls in this source order: previous, five, current six, seven, next. A normal flex row inside an RTL container starts on the right. Reading from that edge follows the same sequence without reversing its data.

Do not reverse the array of page numbers during Arabic rendering. Do not also add row-reverse to an already RTL pager. These changes can produce a visually descending sequence or an order that differs from the source used for keyboard navigation. MDN explains the relationship between row layout and direction in its flex-direction reference.

Numbers inside one label and separate numbered controls are different layout problems. A multi-digit page number keeps its own digit order; separate links are arranged by their parent layout. Turning the entire pager LTR merely to preserve a number sacrifices the intended position of the controls around it.

Inspect the pagination container's computed direction when it appears on the wrong side. A generic LTR toolbar wrapper may override the page's RTL direction. Fix that boundary before compensating with item-specific order values or an extra reversal. RTL flexbox covers the parent-layout diagnosis in more detail.

If the product intentionally retains an LTR pagination convention within an Arabic interface, document and test that decision as a complete component. Its labels, arrows and keyboard sequence must still agree with its behavior. Avoid an accidental combination of an RTL control group and an unrelated LTR number strip.

The following markup represents ordinary navigation to another directory URL. Each neighboring page is a real link, and the current page is noninteractive text. The reserved example addresses preserve the directory's specialty and district filters.

html
<nav class="workshop-pager" lang="ar" dir="rtl"     aria-label="صفحات نتائج ورش الإصلاح">  <ol role="list">    <li>      <a class="workshop-pager__step"         href="https://repair.example/ar/workshops?specialty=clocks&amp;district=harbor&amp;page=5"         rel="prev">        <span class="workshop-pager__arrow" aria-hidden="true">→</span>        <span>السابق</span>      </a>    </li>    <li class="workshop-pager__neighbor">      <a href="https://repair.example/ar/workshops?specialty=clocks&amp;district=harbor&amp;page=5"         aria-label="الصفحة ٥"><bdi>٥</bdi></a>    </li>    <li>      <span class="workshop-pager__current" aria-current="page">        <bdi>٦</bdi>      </span>    </li>    <li class="workshop-pager__neighbor">      <a href="https://repair.example/ar/workshops?specialty=clocks&amp;district=harbor&amp;page=7"         aria-label="الصفحة ٧"><bdi>٧</bdi></a>    </li>    <li>      <a class="workshop-pager__step"         href="https://repair.example/ar/workshops?specialty=clocks&amp;district=harbor&amp;page=7"         rel="next">        <span>التالي</span>        <span class="workshop-pager__arrow" aria-hidden="true">←</span>      </a>    </li>  </ol>  <p class="workshop-pager__summary">    الصفحة <bdi>٦</bdi> من <bdi>١٨</bdi>  </p></nav>

The pager has a translated landmark name distinct from other navigation regions. Numeric links have names identifying them as pages. One item identifies the current page, using the state described in MDN's aria-current reference.

Keeping the current page linked is also a valid design choice. If you do, put aria-current="page" on that link instead. Do not mark every visible page as selected, and do not turn the pager into a tablist simply because it has an active-looking item. Page navigation and tab selection have different interaction models.

Native links support browser behaviors such as opening a destination in another tab and copying its address. The WAI link pattern recommends native anchors for that behavior. A button is appropriate when the component performs an in-place action; use type="button" so an enclosing search form does not submit accidentally, as documented in MDN's button reference.

Make the arrow agree with the action#

In the Arabic markup, previous has a right-pointing arrow and next a left-pointing arrow. Both icons are decorative because visible text names their actions. For the English counterpart, previous points left and next points right. Select those symbols or icon variants from the component's direction; keep the page destinations unchanged.

The arrow's position relative to its label also matters. The previous arrow sits toward the earlier edge of the sequence, and the next arrow toward the later edge. Inspect the whole control rather than flipping an icon without checking its spacing and placement.

Use one mirroring mechanism. If an icon library already supplies RTL variants, adding a second CSS flip can restore the English arrow accidentally. Conversely, an SVG's geometry does not mirror merely because its parent declares RTL. Test the actual icon rendered by the shared component, including first and last variants if present.

An arrow-only pager can work when each control has a translated accessible name. Visible words are useful here because they state whether the action loads another result page, returns to an earlier page or changes something else. An unlabeled arrow leaves that meaning unspecified; an English accessible name inside an Arabic interface leaves part of the control untranslated.

Do not use “left” and “right” as action identities in application code or instructional text. A label such as “next results” can remain accurate when the layout changes; a label describing a physical arrow direction depends on presentation.

Ritla's R025 concerns a directional icon pointing against the sequence its control advances. R011 concerns untranslated interactive labels. These are separate checks: a translated word does not establish that the adjacent arrow or linked destination is correct.

Keep digit formatting out of URLs and arithmetic#

The sample uses Arabic-Indic digits for display while query parameters remain ordinary numeric page values. This is a formatting choice to align with the directory's number policy, not a requirement imposed by RTL direction itself. Western digits can also be a deliberate Arabic product choice.

Set the numbering system explicitly when rendering page labels rather than relying on a locale default:

js
const pageDigits = new Intl.NumberFormat("ar", {  numberingSystem: "arab",  useGrouping: false,  maximumFractionDigits: 0}); pageDigits.format(18); // "١٨"

MDN's Intl.NumberFormat reference documents locale-sensitive formatting. Use the same formatter for numeric controls, accessible page names and the current-page summary. If the product chooses Western digits, select latn consistently instead.

Keep the underlying page as an integer. Build a URL from that value, not from the localized text of a clicked control. Reading textContent and passing it to parseInt confuses presentation with data, especially once the label includes words or localized digits.

For this directory, destination creation can preserve the same filter values while replacing only the page:

js
function workshopPageUrl(page) {  if (!Number.isInteger(page) || page < 1) {    throw new RangeError("Expected a positive page number");  }   const url = new URL(    "https://repair.example/ar/workshops?specialty=clocks&district=harbor"  );  url.searchParams.set("page", String(page));  return url.href;}

The fixed base URL makes this helper runnable as shown. In the application, derive the base from the validated current directory state. MDN documents query manipulation through URLSearchParams.

Isolate a page value separately from the Arabic sentence around it, as the summary markup does. Do not concatenate current and total into an ambiguous run of digits and punctuation. If a design includes a result range as well as a page count, preserve the range's meaning rather than treating every reversed-looking hyphen as an identifier defect. See numbers inside RTL text for that distinction.

Handle the ends of the sequence explicitly#

On the first page, previous has no destination. On the last, next has none. The state helper expresses those boundaries with null, leaving the renderer to omit the unavailable control or show a noninteractive representation according to the design.

Do not link previous to page zero, use a placeholder href="#", or link an unavailable next control back to the current page. Those choices create an action where the interface claims there is no action. If consistent spacing is required, reserve it through layout rather than preserving a misleading destination.

An anchor does not gain native disabled behavior from a disabled attribute. aria-disabled="true" announces a state but does not itself prevent activation. MDN explains the additional behavior required in its aria-disabled reference. For a button-based pager, native disabled has its own behavior; consider focus before disabling the control currently being used.

For a single result page, decide whether the entire pager is useful. A static page summary may be enough. With no results, show the empty-state message instead of a row of unavailable page actions.

When filtering reduces the total below the current page, reconcile the requested page with the new result set. Resetting to the first page or resolving to the last valid page can both be product policies. Update the results, URL, summary and current marker together; do not display page six's marker above page one's records.

Fit the controls without compressing their meaning#

Use content-driven spacing and allow Arabic labels room to grow. The following styles work with the sample markup and leave the current state distinguishable without relying on color alone:

css
.workshop-pager ol {  display: flex;  flex-wrap: wrap;  align-items: center;  gap: 0.625rem;  list-style: none;  margin: 0;  padding: 0;} .workshop-pager li {  min-inline-size: 0;} .workshop-pager a,.workshop-pager__current {  display: inline-flex;  align-items: center;  justify-content: center;  gap: 0.5rem;  box-sizing: border-box;  min-inline-size: 2.75rem;  min-block-size: 2.75rem;  padding-block: 0.375rem;  padding-inline: 0.625rem;} .workshop-pager__current {  border: 2px solid currentColor;  font-weight: bold;} .workshop-pager__arrow {  direction: ltr;  unicode-bidi: isolate;} .workshop-pager a:focus-visible {  outline: 3px solid currentColor;  outline-offset: 3px;} @media (max-width: 30rem) {  .workshop-pager__neighbor:not(:focus-within) {    display: none;  }}

The narrow layout hides only the optional adjacent numeric links. Previous, current and next remain available, with the separate summary giving the total. The breakpoint is illustrative; choose it from the actual labels, font and surrounding space.

Hiding those neighbors is appropriate only because the adjacent destinations remain reachable through previous and next. Do not reuse the class on a unique jump destination without reviewing that loss. The :focus-within exception retains a neighbor while its link has focus, so crossing the breakpoint does not hide that focused link. MDN documents the descendant matching behavior in its :focus-within reference.

That exception can temporarily leave an extra numeric control visible at the narrow width. Allow wrapping to accommodate it, then confirm it disappears when focus moves elsewhere. If your design requires a fixed compact shape, implement an explicit focus transfer before removing the destination instead of dropping the exception without replacing its behavior.

For a larger page window, ellipses indicate omitted positions rather than a page with its own number. Render a decorative ellipsis as noninteractive text. If clicking it opens a page chooser or jumps through a range, make it a named control with that explicit behavior. An unexplained clickable ellipsis introduces an action the page labels do not describe.

Avoid squeezing a long pager into tiny targets or clipping it at the container edge. Reduce the numeric window, retain useful navigation and test the actual compact state. A single unwrapped strip that requires discovering off-screen links is a different design choice, not an automatic consequence of RTL.

Generate a page window before applying direction#

If the directory adds first and last page links, generate that window in ascending order before rendering either locale. This helper uses the earlier paginationState function to validate its inputs. It retains the first page, last page and immediate neighbors, filling a single missing page instead of hiding it behind an ellipsis:

js
function pageWindow(page, totalPages) {  paginationState(page, totalPages);   const candidates = [1, totalPages, page - 1, page, page + 1];  const pages = [...new Set(candidates.filter(value =>    value >= 1 && value <= totalPages  ))].sort((a, b) => a - b);   return pages.flatMap((value, index) => {    if (index === 0) return [value];     const previous = pages[index - 1];    if (value - previous === 2) return [previous + 1, value];    return value - previous > 2 ? ["gap", value] : [value];  });}

For the directory's middle page, the output is [1, "gap", 5, 6, 7, "gap", 18]. This is an optional larger presentation than the three-number sample, not a second ordering convention. The Arabic renderer uses that same array and its RTL parent layout.

The "gap" token is a rendering instruction, not a page identifier. Create a noninteractive ellipsis for it; create page destinations only for integer tokens. Give the two gap positions distinct rendering keys if your framework requires them. Reusing one key for both can obscure which item should survive an update.

This algorithm prioritizes specific destinations rather than promising a fixed number of controls. Its output changes near the sequence boundaries, and short result sets can expose every page. Choose another window policy if the design requires a stable control count, but retain the same ascending sequence and validate its first, middle and last states.

Do not infer current state from the middle array position. On a boundary page, that position can hold another page number or an ellipsis. Compare each numeric token with the actual current page, and apply the current marker once.

Coordinate results, focus and loading state#

For ordinary page loads, the destination URL determines the new page. For client-side pagination, the interface must coordinate a request, the rendered results and the control state. That coordination should not depend on the arrow's screen direction.

Decide whether focus remains on the activated control or moves to the refreshed result heading. Retaining focus supports repeated paging; moving it can help users begin reading new results. Apply a consistent policy and test its consequences with a screen reader. Rebuilding the pager without managing focus can discard the focused link while giving no indication that anything changed.

Expose a concise result-update message when content changes without moving focus, using a status region suited to the interaction. Announce the completed result state rather than making the entire listing a live region. The WCAG status-message guidance explains the distinction between an update and a focus change.

If a region is marked busy during replacement, clear that state after success or failure. aria-busy communicates an update state; it does not cancel requests, prevent clicks or enforce request ordering.

Handle rapid activation explicitly. If page seven's request finishes after page eight's, it must not replace the records while the pager still marks eight as current. Cancel superseded work or ignore responses that no longer match the requested state. On failure, retain or restore a coherent successful page and expose a retry action rather than leaving the marker ahead of the results.

Keep sort, filters and page size in the pagination contract. Changing page size can change which page contains the same record, while changing a filter can eliminate the current page. Test those transitions with the same state model used for direct page navigation.

Also test browser navigation independently of repeated next clicks. Open a numeric link in a new tab and reload its URL: it should recover the same directory filters and requested page without relying on the original tab's component memory. Use browser Back after changing pages and confirm the URL, records and current marker agree again.

For a client-side router, use its history integration for that restoration rather than updating only the visible page counter. Decide whether changing a filter creates a new history entry or replaces the current one, then test paging after that change. A language switch should preserve the intended filter and page context or apply an explicit reset policy, not silently combine an old page number with a new unfiltered directory.

Test destinations as well as the mirrored screenshot#

Use Arabic and English fixtures with the same filters, result ordering and current page. Verify the middle of the sequence first, then its boundaries. A screenshot of page one cannot demonstrate that both directions work.

  1. Middle page: activate next from six and confirm page seven's actual records, URL, summary and current marker. Activate previous and confirm the expected return. Repeat in both locales.
  2. Arrows and labels: check next and previous independently. Confirm the icon, visible wording, accessible name and destination describe the same operation, including arrow-only compact variants.
  3. Boundaries: exercise first, last, single-page and empty results. Confirm unavailable actions have no accidental destination or keyboard activation.
  4. Digits: compare numeric controls, accessible names and summaries. Confirm the chosen numbering system is consistent while URL state remains usable after reload and direct entry.
  5. Keyboard and width: follow the source sequence with Tab, inspect visible focus, narrow the viewport and zoom. Check focus when optional numeric links disappear.
  6. Data transitions: change filters, sort and page size. For asynchronous results, delay responses, activate controls rapidly and cause a failed request. Confirm the result state never disagrees with the pager.

Check top and bottom pagers together if the directory renders both. They should share the same current state and destinations without duplicate element IDs. Updating only the one that received the click leaves another apparently valid control advertising stale navigation.

Run a free Ritla scan to check pagination for a directional icon pointing against the sequence its control advances with R025, and mixed Arabic-Indic and Western digits in the same context with R061. If layout order still disagrees with keyboard order, continue with RTL flexbox; if numeric summaries are the remaining problem, use numbers inside RTL text.

Checks in this guide

See what your Arabic pages are hiding

Paste a URL. Ritla renders the page on desktop and mobile, runs every check, and shows the top issues with screenshot evidence.

Scan a page freeBrowse the checks