RTL breadcrumbs: order and separators
Build RTL breadcrumbs with the right hierarchy order, directional separators, isolated mixed-language labels and usable layouts at phone widths.
By Omran Khleifat, Founder of Ritla
Guide13 min read
A breadcrumb can look mirrored while describing the hierarchy backward. Reversing the route array, reversing the flex row and changing the separator are three different operations. Applying all three to an Arabic trail can put the current page at the reading start and leave keyboard navigation following a different sequence.
Keep the hierarchy in its ordinary order: root, ancestors, current page. Let the component's direction determine where that sequence begins. On a single horizontal Arabic row, the root appears on the right and the current page toward the left. Once the trail wraps, the current page may appear on a later line, so “current page on the left” is not a reliable implementation rule.
The examples below belong to a water-quality data catalogue. Its trail runs from the home page through water data and coastal stations to sampling reports. The same hierarchy serves both languages; translated labels and direction change its presentation.
Preserve the path from root to current page#
Breadcrumbs describe a place in a hierarchy. They do not record every page visited before arriving there. A user opening a sampling report from a search result should still see its catalogue ancestors, rather than a trail assembled from browser history.
For this catalogue, the intended sequence is home, water data, coastal stations, sampling reports. In Arabic, a reader starts at the right and moves through that sequence toward the left. A root-to-leaf array therefore stays root-to-leaf when the locale changes.
Do not use reverse() as a translation step. That changes the meaning of the data before the browser has laid anything out. If another part of the application also consumes the array, an in-place reversal can affect its behavior too. Keep stable destination identities and translate their names separately.
The WAI breadcrumb pattern defines a trail through parent pages in hierarchical order. Whether the current page is a link is a separate choice. Its position in the hierarchy remains last either way.
Some records have several possible parents. Choose the trail from an explicit information-architecture rule, such as the catalogue collection owning the record. If the product deliberately shows a context-dependent path, retain that context when following its ancestor links. Changing the visible parent without changing the linked destination produces a navigation defect that no direction rule can repair.
Remove the second reversal from the layout#
An RTL flex row already begins at the right. A rule originally used to fake that placement on an English page can reverse it again:
Problem
.water-trail ol { display: flex; flex-direction: row-reverse;}With root-first source markup inside an RTL container, this lays the items out left to right in source order. The root has moved to the opposite edge from the one the Arabic design intended. Reversing the data to compensate introduces another dependency rather than resolving the layout rule.
Better
.water-trail ol { display: flex; flex-direction: row;}The ordinary row follows the container's direction, as MDN explains in its flex-direction reference. In English it starts at the left; in Arabic it starts at the right. Source order does not need to change.
This correction targets a trail whose intended English layout is root-first from the left. If the existing English page relied on reversed source data, repair that data and its presentation together rather than assuming a one-line change preserves its current appearance.
Inspect the ordered list's computed direction, not just the document's language. A local LTR wrapper can interrupt inheritance. Conversely, styling Arabic text with right alignment does not establish the direction of the flex row. Resolve the component boundary before investigating the separator.
Ritla's concerns flex row-reverse faking RTL instead of dir, with tab order fighting visual order. It does not verify that your route model contains the correct ancestors; inspect that separately.
Mark up destinations and separators separately#
Use a named navigation region containing an ordered list. Here, the current page is text rather than a link back to itself. The URLs are reserved examples representing the catalogue's Arabic routes.
<nav class="water-trail" lang="ar" dir="rtl" aria-label="مسار التنقل"> <ol role="list"> <li> <a class="water-trail__label" href="https://water.example/ar/">الرئيسية</a> </li> <li> <span class="water-trail__separator" aria-hidden="true"></span> <a class="water-trail__label" href="https://water.example/ar/data">بيانات المياه</a> </li> <li> <span class="water-trail__separator" aria-hidden="true"></span> <a class="water-trail__label" href="https://water.example/ar/data/coastal">محطات الساحل</a> </li> <li> <span class="water-trail__separator" aria-hidden="true"></span> <span class="water-trail__label" aria-current="page"> تقارير أخذ العينات </span> </li> </ol></nav>The separator belongs to the item following it. Keeping those two pieces inside the same list item prevents a separator from wrapping as an independent flex item while its destination moves elsewhere. It also makes separator count predictable: one before every item except the first, with none after the current page.
The links retain native link behavior. The current text does not need tabindex, a disabled-link role or a click handler that does nothing. If your product keeps the current item linked, put aria-current="page" on that link instead. Use a real destination rather than href="#".
MDN explains the current-page state in its aria-current reference. Apply it to the current item only; ancestors can be relevant to the page without also being current.
In the full application, put language and direction on the document root and inherit them where appropriate. The local declarations make this example self-contained. The navigation's Arabic accessible name distinguishes it from the primary navigation; MDN's nav reference covers naming multiple navigation regions.
The explicit list role preserves that intent while the following CSS removes list markers. MDN documents accessibility considerations for list-style: none in its list-style reference. Verify the resulting list announcement with the browser and assistive technology combinations you support.
Point directional separators through the hierarchy#
A separator separates ancestors from descendants. It is not a Back button. A chevron used to indicate progress from root toward the current page points right in a horizontal English trail and left in its Arabic counterpart. Moving the items without reviewing the artwork leaves those two signals in disagreement.
The direction of that decorative progression does not describe every click. Activating an ancestor takes the visitor up the hierarchy, while the trail itself is drawn from the root down to the current location. Do not give each ancestor a separate back arrow to reconcile those two perspectives. Its position and label already identify its relationship to the current page.
A browser-history Back control also has a different destination contract. It may return to a search page outside the catalogue rather than to the coastal-stations parent. Keep any “back to parent” action distinct from history navigation, even if both appear near the breadcrumb.
The base layout keeps spacing relative to the items rather than assigning a physical margin to every separator:
.water-trail ol { display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.75rem 1rem; list-style: none; margin: 0; padding: 0;} .water-trail li { display: flex; align-items: center; gap: 0.625rem; min-inline-size: 0; max-inline-size: 100%;} .water-trail__label { min-inline-size: 0; overflow-wrap: anywhere;} .water-trail a:focus-visible { outline: 3px solid currentColor; outline-offset: 3px;}For a small directional triangle, draw the two states explicitly. This is an alternative to a chevron asset, using the empty separator spans already in the markup:
.water-trail__separator { flex: none; inline-size: 0; block-size: 0; border-block: 0.25rem solid transparent; border-left: 0.375rem solid currentColor;} .water-trail:dir(rtl) .water-trail__separator { border-left: 0; border-right: 0.375rem solid currentColor;}The physical borders describe the icon's geometry and have an explicit RTL counterpart. They do not position the breadcrumb items. The default triangle points right; the RTL rule replaces it with a left-pointing triangle. Check that your product's global border styles do not override either state.
The selector follows the navigation region's direction, not the language or direction of an individual label. MDN's :dir() reference explains that it follows semantic directionality rather than CSS direction alone. A component configured only through CSS can therefore display an RTL row while missing this selector's override.
If you use an existing chevron asset, inspect whether the icon component already mirrors it. Applying another flip can restore the English orientation accidentally. Change only the directional separator; reflecting the entire breadcrumb would also reflect its text and any nondirectional symbols.
Do not cite a control-arrow check as evidence that every static separator has been verified. Ritla's directional-control wording does not make a decorative breadcrumb marker an interactive next-step control. Review the marker visually in each locale.
Keep slashes decorative and outside link names#
A slash can remain the same separator in both languages. Unlike an arrow, it does not inherently indicate movement toward a child page. Its role here is a boundary between items, not a directional instruction.
To use slashes, replace each empty separator span with the following and omit the triangle rules:
<span class="water-trail__separator" aria-hidden="true">/</span>.water-trail__separator { flex: none;}Keep the symbol in its own element rather than concatenating the entire trail into one string. Individual labels remain separate destinations, and a separator is not included in either link's accessible name. The grouped layout also avoids asking punctuation in a single mixed-direction paragraph to establish the hierarchy's visual structure.
Separators do not need to be announced between every ancestor. The list structure and destination names carry the relationship; aria-hidden="true" excludes these decorative spans. Do not apply it to the list item or another ancestor containing a link.
Generated CSS content is another way to draw a separator, but a slash inserted with content is not an ordinary DOM node to which you can attach aria-hidden. Review its accessible output rather than assuming all generated punctuation is silent. The explicit spans above make that boundary inspectable.
Neither a slash nor a chevron should become an extra tab stop. A hidden-ancestors disclosure button is a different element with a different job; do not style a decorative separator to look clickable unless it actually performs that job.
Isolate a label without changing the trail's direction#
A data catalogue may retain an English project name in an Arabic hierarchy. That does not make the complete breadcrumb LTR. Keep the outer list's Arabic direction and isolate the label or name at the appropriate boundary.
For a whole label supplied by catalogue metadata, wrap its text separately from the separator:
<a class="water-trail__label" href="https://water.example/ar/projects/siltlab"> <bdi dir="auto" lang="en">SiltLab+</bdi></a>Here the project name's language is known. If a metadata label can be Arabic or English, derive any language annotation from reliable metadata rather than copying lang="en" onto every dynamic label. Direction detection and language identification are separate decisions.
Do not put dir="auto" on the whole breadcrumb list to accommodate one English name. Its first strong character can determine the component's direction, changing the sequence's starting edge based on content. Put the automatic boundary around the label itself.
Isolation of the whole label does not resolve every mixed phrase inside that label. Consider a name embedded between Arabic words:
Problem
<span class="water-trail__label">تقارير SiltLab+ الشهرية</span>The Latin name has the trailing-plus shape that can lose its punctuation to the other side between Arabic words. An ordinary span supplies no isolation. The fix belongs around the name, preserving the Arabic phrase around it:
Better
<span class="water-trail__label"> تقارير <bdi lang="en" dir="ltr">SiltLab+</bdi> الشهرية</span>MDN documents the isolation boundary in its bdi reference. For more involved metadata labels, see bidi isolation in HTML. The breadcrumb still needs ordinary layout direction; text isolation and item placement solve different problems.
Inspect the visible punctuation as well as the copied text. Copying can retain logical source order even when a label's displayed punctuation has moved. A screen reader pass also cannot substitute for checking that display.
Let long trails wrap before hiding destinations#
The sample layout wraps whole items onto subsequent lines while permitting an unusually long label to break inside its available width. Normal word boundaries remain preferable; overflow-wrap: anywhere provides an escape for an otherwise unbreakable token. See MDN's overflow-wrap reference.
This is different from forcing every breadcrumb into a single line and hiding the overflow. That can remove the current page or an ancestor without giving the visitor another way to reach it. An invisible link may still be a keyboard stop, depending on how it was concealed.
Because the separator is grouped with the following label, a new flex line can begin with that item's separator. That is a tradeoff of the wrapping design, not an unexpected extra ancestor. If the product forbids leading separators on continuation lines, choose a layout that accounts for line boundaries or collapse the trail deliberately. A simple :first-child rule knows the first source item, not the first item on every rendered line.
Do not add wrap-reverse to make the Arabic row feel more reversed. It changes line stacking. With ordinary wrapping, the next group of ancestors belongs below the preceding group, retaining the hierarchy's progression. MDN distinguishes these behaviors in its flex-wrap reference.
Use a long current label such as تقارير أخذ العينات الدورية لمحطات الساحل الشمالي when checking the component. Confirm the separator remains distinct from the text, the final words are visible, and focus outlines around preceding links are not clipped by the navigation container.
If the design truncates labels instead, decide which information may disappear. Two coastal sections with a shared opening phrase may become indistinguishable when both receive the same ellipsis treatment. Keeping the complete text in the accessible name does not give a sighted touch user that missing distinction. Provide an approved shorter label or an interaction that reveals the complete name without requiring pointer hover.
Do not shorten a label by slicing arbitrary characters from the translated string. A character budget is not a measurement of the space occupied by the production font, and the resulting fragment may no longer name the destination clearly. Keep any navigation-specific short wording in the content model, alongside the full page title, and verify it in context.
The entire trail need not be one large unbreakable token. Avoid inheriting white-space: nowrap from a generic toolbar without reviewing its effect. Test at a width equivalent to 320 CSS pixels and with enlarged text; WCAG's reflow guidance provides the narrow-width context.
Collapse ancestors as an interaction, not an ellipsis#
For a deep catalogue hierarchy, showing every ancestor can dominate a phone screen. A disclosure that hides intermediate ancestors can be a deliberate alternative to wrapping. Keep the root, a useful nearby parent and the current location visible according to the product's navigation needs.
Make the hidden-ancestors control a real button with an Arabic accessible name describing what it reveals, rather than a bare ellipsis character with a click handler. Expose its expanded state and associate it with the revealed region. The revealed ancestors remain links in root-to-leaf order.
Choose an interaction model before styling the popup. A small disclosure of website links can use native Tab navigation; it does not automatically need an ARIA menu role or arrow-key navigation. If it becomes a modal on small screens, it needs the corresponding modal behavior. Do not combine those models based only on visual resemblance.
Keep a visible way to restore context after expansion. If the disclosure disappears because all ancestors are now inline, decide where its keyboard focus goes before removing it. Retaining the button as an expanded control is one option; moving focus into the revealed ancestors is another to evaluate with the complete interaction.
When the viewport widens, do not leave the same ancestors active both inside a hidden-looking popup and in the inline trail. Render or expose the intended presentation consistently, and check focus during that transition. Collapsing for available space should not rewrite the hierarchy or change which page is current.
Keep labels and destinations synchronized during routing#
Build the trail from the application's route or content model rather than splitting the visible URL at every slash. Route segments can include language prefixes, technical identifiers and grouping that users should never see as destinations. Some segments do not represent navigable parent pages at all.
Treat the displayed label, linked URL and ancestor identity as one record. On a locale switch, translate the label and resolve the matching destination together. An Arabic trail that links into the English catalogue can look correct until the first ancestor is activated.
Do not repurpose the trail as a list of active filters. A selected station type or date range can change the report without becoming another hierarchical ancestor. If a filter intentionally maps to a browse category, define that mapping instead of appending every query parameter to the breadcrumb.
For asynchronously loaded report names, reserve a loading treatment without inventing a linked placeholder parent. Once the data arrives, update the current item and page heading coherently. Avoid reconstructing the whole navigation region solely to replace one label if doing so would discard focus on an ancestor link.
Test direct entry to a report URL, browser Back, a locale switch and navigation from a search result. Each should produce the intended hierarchy, even though the user's arrival path differs. These tests establish the trail's meaning; changing direction cannot establish it.
Verify hierarchy, geometry and announcements separately#
Keep an English fixture with translated labels and the same destination identities. Compare relationships rather than expecting the Arabic screenshot to be an exact pixel reflection: label lengths and line breaks can legitimately differ.
Check these states in the browsers your product supports:
- Hierarchy: inspect source order from root to current page. Confirm each ancestor's URL and label agree, including after a direct page load and locale switch.
- Direction: on a single row, confirm the Arabic root is at reading start. Look for local direction overrides,
row-reverse, itemorderrules or reversed route data. - Separators: check every gap, including before the current item. Confirm directional artwork points through the hierarchy, slashes remain decorative and no trailing marker implies another destination.
- Mixed labels: inspect the complete project name and its punctuation. Keep the outer trail RTL while the isolated English name reads as intended.
- Narrow layout: force wrapping with the long current label. Check continuation lines, full label visibility and focus outlines. If ancestors collapse, open them using the keyboard and resize while the disclosure has focus.
- Accessibility: confirm the named navigation region, list, ancestor links and current-page state. Decorative separators should not become spoken link names or additional tab stops.
With CSS disabled, the hierarchy should still read root-first and retain usable ancestor links. With styles enabled, verify the same relationship visually. Neither result alone proves the other, and a correct accessible announcement does not establish the visual order of mixed text.
For a suspected ordering failure, run this in DevTools on the sample component. It reports source position alongside each item's physical bounds:
const trailItems = [...document.querySelectorAll(".water-trail ol > li")]; console.table(trailItems.map((item, index) => { const bounds = item.getBoundingClientRect(); return { sourcePosition: index + 1, label: item.querySelector(".water-trail__label").textContent.trim(), top: Math.round(bounds.top), left: Math.round(bounds.left), right: Math.round(bounds.right) };}));getBoundingClientRect() supplies viewport-relative physical coordinates. On a single RTL row, compare successive items from the right. On a wrapped trail, inspect each line separately before comparing horizontal positions. A later ancestor can legitimately have a larger right coordinate after wrapping to the next line; sorting the entire trail by horizontal position would misdiagnose that layout.
The table is diagnostic evidence, not a complete accessibility or rendering test. Different item heights can affect their top coordinates, and a rectangle does not establish that the words or punctuation inside it are readable. Use the geometry to locate the suspect item, then inspect its label, separator and computed styles.
Use a free Ritla scan when checking a clipped trail: concerns clipped text whose content is wider than its box with overflow hidden and no ellipsis. For a label that disappears at phone width, continue with Arabic text getting cut off in CSS; for an intact label whose punctuation moves, use Arabic and English mixed text.