RTL navbar design and implementation
Build an RTL navbar with logical spacing, readable Arabic labels, positioned dropdowns and a mobile menu that preserves keyboard focus.
By Omran Khleifat, Founder of Ritla
Guide13 min read
A navbar can put its logo on the right and still fail as an Arabic header. The primary links may crowd the action button, a dropdown may extend beyond the viewport, or the compact menu may hide whichever link currently has keyboard focus. Mirroring the first screenshot does not resolve those states.
Start by deciding what the header should do. In the bird-observation portal used here, the brand belongs at the reading start, the primary links follow it, and the action for adding an observation sits at the opposite end. On a narrow screen, those same links move into an expandable panel below the brand. An Arabic reader encounters the header from the right; an English reader encounters its counterpart from the left.
That arrangement needs semantic direction, logical spacing and a responsive state model. It does not require reversing the link array or flipping the entire header with a transform. A mobile panel entering from the left is also not inherently broken: its expected edge depends on the design. The defect is an implementation that contradicts that design, hides content or moves it outside the usable viewport.
Define the groups before positioning them#
Treat a navbar as a small layout with several responsibilities. The brand identifies the site. Primary links expose its destinations. An account control or contribution action serves a separate task. The compact-menu button reveals destinations when the complete row no longer fits.
Write those groups in a meaningful source order before adding alignment rules. For this portal, that is brand, menu button, primary navigation, contribution action. The button appears only in the compact layout; the other content keeps its position in the document.
Avoid building separate desktop and mobile copies of the destinations. Two copies create two places to maintain the current-page state, accessible labels and permissions. They also complicate focus handling when the viewport changes. One set of links can serve both layouts unless the product actually requires different navigation content.
The Arabic document should declare lang="ar" and dir="rtl" on its root element. Language identifies the content; it does not establish layout direction. A standalone component example can declare both on its own boundary, as below. In an application, prefer inheriting the document direction unless the component intentionally differs. See language versus direction when the page has Arabic labels but still lays out from the left.
Keep the logo artwork separate from the layout decision. Moving a logo to inline start does not mean reflecting its lettering or symbol. The example uses a text brand so the directional boundary is visible without an image asset. A production image needs its own suitable alternative text and dimensions, but should not acquire a negative scale simply because its container is RTL.
Fix the spacing rule, not the link order#
Suppose an English header separates its contribution action from the navigation with an automatic left margin:
Problem
.birdbar__action { margin-left: auto;}The intent is to consume spare space before the action, leaving it at the reading end of the row. A physical left margin continues to mean the left side in Arabic. It does not become the space between the preceding navigation and the action merely because the parent changes direction.
Better
.birdbar__action { margin-inline-start: auto;}In a horizontal English layout, inline start is left, so this replacement preserves the original arrangement. In Arabic, inline start is right. The automatic margin now separates the same two groups relative to the reading direction. MDN documents that mapping for margin-inline-start.
Use gap for regular spacing between adjacent links. Reserve the automatic margin for distributing remaining space between groups. Combining an arbitrary margin on every link with a separate rule for the last child makes the header harder to reason about when links disappear after a permission change.
Leave the flex row in its normal direction. Under RTL, its first item starts at the right already. Adding row-reverse changes that relationship again without changing the source sequence used by keyboard navigation. If a particular group intentionally has a different order, document that requirement rather than applying reversal to the whole header.
MDN's flex-direction reference explains how row follows the container's direction and why reverse layouts can disconnect presentation from source order. Verify the complete component in your supported browsers, including its responsive and interactive states.
Build the header with one set of destinations#
This example belongs to a fictional bird-observation portal. Its reserved example URLs represent ordinary page navigation. Place the script later in the document, after this markup, or load it with defer.
<header class="birdbar" lang="ar" dir="rtl"> <a class="birdbar__brand" href="https://birds.example/"> راصد الطيور </a> <button id="birdbar-toggle" class="birdbar__toggle" type="button" aria-expanded="false" aria-controls="birdbar-panel" hidden> القائمة </button> <div id="birdbar-panel" class="birdbar__panel"> <nav aria-label="التنقل الرئيسي"> <ul class="birdbar__links"> <li><a href="https://birds.example/observations" aria-current="page">المشاهدات</a></li> <li><a href="https://birds.example/trails">المسارات</a></li> <li><a href="https://birds.example/species">دليل الأنواع</a></li> </ul> </nav> <a class="birdbar__action" href="https://birds.example/submit"> إضافة مشاهدة </a> </div></header>The destinations remain links. The menu opener is a button because it changes the visibility of existing content. The navigation landmark has an Arabic accessible name, and the current destination is identified independently of its visual treatment.
Do not add role="menu" merely because the product calls this a menu. That role brings interaction expectations associated with application menus. A collection of website links with a disclosure button can keep native link and button behavior. The WAI disclosure navigation example explains this distinction.
The panel starts visible and the button starts hidden. Without JavaScript, visitors can still reach every destination. Enhancement adds the collapse behavior after it can manage the button and panel together. Starting with a hidden panel would make navigation depend on the script loading successfully.
Use unique IDs for each instance. If a page includes another header in a preview or embedded application, copying these IDs would leave aria-controls ambiguous. Component frameworks should generate stable instance IDs that also survive server rendering and hydration.
Give Arabic labels room to participate in layout#
The wide layout below groups the links and action in a flexible panel. It avoids a fixed header height and lets link rows grow vertically while the compact layout takes over at the chosen breakpoint.
.birdbar { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; padding-block: 0.875rem; padding-inline: 1rem;} .birdbar [hidden] { display: none;} .birdbar__brand { flex: none;} .birdbar__panel { display: flex; flex: 1; align-items: center; gap: 1rem; min-inline-size: 0;} .birdbar__panel nav { min-inline-size: 0;} .birdbar__links { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1rem; list-style: none; margin: 0; padding: 0;} .birdbar__action,.birdbar__toggle { margin-inline-start: auto;} .birdbar a,.birdbar button { font: inherit; text-align: start;} .birdbar a:focus-visible,.birdbar button:focus-visible { outline: 3px solid currentColor; outline-offset: 3px;}These are the layout rules, not a complete visual theme. Add your colors, link treatment and button padding through the product's existing styles, then include their dimensions when choosing the breakpoint. Preserve a visible current-page treatment as well as aria-current.
There are two details worth keeping during that integration. First, flexible items can retain an automatic minimum based on their contents. A long label can therefore resist shrinking even when its parent has spare flexibility elsewhere. min-inline-size: 0 allows the relevant flex items to shrink along the inline axis. MDN explains automatic minimum sizing on its min-width reference.
That permission does not make the content fit by itself. Links must still wrap, move to the compact layout or otherwise remain available. Applying zero minimum size while retaining clipping can merely conceal the failure inside a smaller box.
Second, author CSS can override the display behavior of the HTML hidden attribute. Without the scoped hidden rule, the panel's display: flex declaration can keep it rendered after the script sets panel.hidden = true. The explicit rule gives the component's hidden state precedence over its ordinary layout rule. This interaction is documented in MDN's hidden reference.
Keep that rule after any competing declarations of equal specificity, and inspect the final cascade if your design system uses stronger selectors or !important. An attribute changing in DevTools is not proof that the associated content has disappeared.
Choose the compact layout from the content budget#
A phone label is not a breakpoint calculation. The header needs room for the brand, every visible destination, the contribution action, their padding and the gaps between them. Account controls and language selection consume that same budget if your product includes them.
Translate the labels before deciding where the wide layout stops. Include the longest approved navigation wording, the production Arabic font and authenticated states. Do not shorten an Arabic destination solely to preserve a breakpoint inherited from the English header.
For this example, 58rem is an implementation choice to evaluate, not a recommended threshold for every navbar:
@media (max-width: 58rem) { .birdbar__panel { flex-basis: 100%; display: block; } .birdbar__links { display: block; } .birdbar__links a { display: block; padding-block: 0.625rem; } .birdbar__action { display: inline-block; margin-inline-start: 0; margin-block-start: 0.75rem; }}The expanded panel occupies a new row beneath the brand and toggle. Its destinations follow the same document order as on desktop. No absolute positioning, horizontal transform or duplicated link list is necessary for this design.
Allowing a wide navbar to wrap briefly can prevent content loss while fonts or state change. If the design requires exactly one row, collapse before wrapping becomes necessary and verify that threshold with the actual content. white-space: nowrap across every label plus a fixed header height is a constraint combination to investigate, not a fix for translation length.
Test a long label such as المسارات المناسبة لمراقبة الطيور المهاجرة in the trails destination. Look at the link's own box, its navigation container and the whole header. The first element that cannot accommodate the text tells you where to investigate sizing; the visible collision may occur farther along the row.
Preserve focus while the menu changes state#
Opening a compact menu is only one transition. A visitor can resize a window while focused inside it, rotate a device, or zoom until the layout crosses the breakpoint. Hiding the panel at that moment needs a focus decision.
The script below uses the same media query as the CSS. It keeps focus on the opener when the compact panel closes. If resizing hides a focused panel link, focus moves to the now-visible opener. If resizing removes the opener from the wide layout, focus moves to the first destination before that button is hidden.
const bar = document.querySelector(".birdbar");const toggle = document.querySelector("#birdbar-toggle");const panel = document.querySelector("#birdbar-panel");const compact = window.matchMedia("(max-width: 58rem)");let open = false; function renderBar() { const small = compact.matches; const active = document.activeElement; if (small && !open && panel.contains(active)) { toggle.hidden = false; toggle.focus(); } if (!small && active === toggle) { panel.hidden = false; panel.querySelector("a")?.focus(); } toggle.hidden = !small; panel.hidden = small && !open; toggle.setAttribute("aria-expanded", String(small && open));} toggle.addEventListener("click", () => { open = !open; renderBar();}); bar.addEventListener("keydown", event => { if (event.key === "Escape" && compact.matches && open) { event.preventDefault(); open = false; renderBar(); toggle.focus(); }}); compact.addEventListener("change", () => { open = false; renderBar();}); renderBar();matchMedia evaluates the query; the media query list's change event handles crossings without a separate width calculation. Changing the CSS breakpoint without changing this query would split the visual layout from its interaction state. Keep both values together in your component's source or build configuration.
Opening leaves focus on the button, so Tab proceeds into the revealed links. The panel is an in-flow disclosure: focus can continue to the rest of the page. Do not trap focus inside it. A full-screen modal navigation requires a different interaction model, including treatment of the background and restoration of focus when dismissed.
This example assumes ordinary page loads when links are activated. In a client-side router, close the panel as part of the navigation lifecycle and place focus according to the destination page's focus policy. Returning focus to an obsolete header button after every route change can interrupt that policy.
The first-link fallback also assumes this panel contains its three destinations. If permissions can remove them all, choose another visible destination for focus before hiding the toggle. The focus target must exist in the actual state being rendered.
Load the intended direction on the first render#
A navbar often appears before the rest of the application has finished loading. Resolve its initial locale, labels and direction together. Serving an English header and changing only its text to Arabic after startup creates an intermediate state that is worth testing explicitly, even when the final screenshot looks correct.
For server-rendered pages, inspect the returned HTML as well as the hydrated DOM. Confirm that the Arabic route already declares its direction and that client initialization agrees with it. For a client-rendered application, check the loading shell and the completed header separately. A skeleton with a physically positioned logo can have a different alignment problem from the eventual navigation.
When switching locale without a page load, update translated accessible names alongside visible labels. The menu button's text, navigation landmark name and account actions belong to the same language state. An Arabic caption with an English aria-label still needs correction; the label spoken by assistive technology is part of the control.
Keep destination identity stable during that update. Reversing the array, rebuilding the panel and resetting focus are three different operations; changing language does not itself require all three. If your framework replaces the active control, verify where focus ends up rather than assuming a preserved visual position preserves interaction.
Font loading deserves its own pass. Compare a delayed-font load with a warm-cache reload at a width close to collapse. Measure the available header space after the actual font and user state are present. If that reveals a collision, adjust the layout budget or collapse threshold instead of reducing Arabic letter spacing to force a fit.
Position dropdowns against their trigger#
A category dropdown adds another coordinate system to the header. The parent navbar can be correct while the popup retains left: 0 from its English implementation. Decide whether the popup should align with the trigger's inline start or inline end, then express that relationship on the popup's positioned container.
For a start-aligned dropdown, use a positioned wrapper around its trigger and popup, then inset-inline-start: 0 on the popup. For an end-aligned account popup, use the corresponding inline-end property. This is a relationship choice, not a search-and-replace rule: converting every physical edge to start can move a popup in English too.
Test both outer edges of the header with the longest menu contents. A popup anchored correctly to its trigger can still need collision handling when its width exceeds the remaining viewport space. Alignment and collision avoidance solve different problems. Absolute positioning in RTL covers the containing-block details.
If a library portals the popup outside the header, inspect direction at the rendered popup rather than only at the trigger. A local dir="rtl" boundary is no longer an ancestor after portaling. Supply direction through the library's documented mechanism or the popup's own boundary. Do not rewrite physical coordinates produced by a positioning engine unless its API requires that conversion.
A disclosure arrow also needs a defined meaning. An arrow indicating expansion is different from a next-step arrow. Check its open and closed states against the chosen interaction; reflecting every SVG in the header can corrupt brand artwork and unrelated icons.
Treat sticky height and animation as separate decisions#
Making the header sticky does not freeze its height. Font loading, a longer account label or opening the compact panel can change the space it occupies. A hardcoded content offset derived from the English screenshot can then leave a destination heading or focused control behind the header.
Keep the navbar in normal layout while deciding whether stickiness is needed. When it is, inspect the scrolling ancestor and the sticky inset together; the position reference describes the ancestor relationship. An overflow rule on a surrounding shell can change which ancestor matters.
For scroll-to-section navigation, evaluate scroll-padding-block-start on the actual scrolling container, using the current obstruction rather than an assumed fixed height. MDN describes this property as an offset for the scrollport's viewing region in its scroll-padding-block-start reference. Test keyboard focus and fragment navigation separately; adding an offset is not evidence that either is unobscured.
The compact panel above does not slide sideways. If the design instead calls for a side panel, specify its entry edge in both locales before implementing the motion. Physical horizontal translations retain their physical direction. Verify the panel's final position, its dismissed position and whether off-screen descendants remain focusable.
Ritla's concerns translateX moving the element off-screen in RTL. It does not establish a universal side from which Arabic navigation must enter. A motion preference, an intentional closed position and an accidentally unreachable open panel are separate cases to review.
Verify the complete navbar state, not just its direction#
Use an Arabic route with production styles and fonts. Also keep an English fixture using the same component, translated labels and dir="ltr". A component that needs a new DOM order for each locale deserves closer inspection.
Run these checks in the browsers your product supports:
- Wide layout: confirm the brand, destinations and action occupy their intended groups. Resize slowly near the collapse threshold. Inspect wrapping before and after the Arabic font finishes loading.
- Compact layout: open and close the panel with the button and Escape. Confirm the destinations remain usable and collapsed links are absent from the Tab sequence. Disable JavaScript and reload to verify the fallback links remain available.
- Breakpoint focus: focus a destination on the wide layout and narrow the window. Then focus the compact opener and widen it. Track the visible focus indicator through each transition.
- Content changes: exercise signed-in controls, permissions and long approved labels. Check that the current destination remains marked after navigation and a locale switch.
- Zoom and narrow widths: evaluate reflow at the equivalent of a 320 CSS-pixel-wide viewport. Look for document scrolling, overlapping labels and clipped focus outlines, including while the compact panel is open.
- Dropdown boundaries: open each popup near both viewport edges. Confirm its trigger relationship, content direction and keyboard dismissal in the rendered location.
The WCAG reflow guidance supplies the narrow-width context. Its focus-order guidance concerns preserving meaning and operation, not requiring Tab to follow every pixel of a visual arrangement. A screenshot and a keyboard pass answer different questions; keep both.
Also test with a screen reader and browser combination your team supports. Confirm that the opener exposes its expanded state, the primary navigation has the intended name, and closed destinations are unavailable. Passing a keyboard test does not establish those announcements. Conversely, hearing every destination does not establish that each label is visible and readable on screen.
Record failures as a state and transition, not just a screenshot. For example: Arabic route, signed in, compact panel open, focus on the trails link, then widen past the breakpoint. Include the expected focus destination and what actually happened. That report distinguishes a bad width threshold from a state-update bug and makes the regression reproducible.
For screenshot comparisons, capture the same route and account state in both locales at the same viewport dimensions. Compare group relationships and available destinations rather than expecting a pixel reflection. Arabic label widths and a different brand asset can legitimately change the occupied space. A reflected screenshot cannot tell you whether the translated header has the right content or interaction.
When investigating an overlap, temporarily outline the brand, panel and action in DevTools. Inspect each box's inline size and minimum size, then disable the suspect constraint one at a time. Hiding horizontal overflow on the document can conceal an off-screen destination while leaving the underlying width problem intact.
After exercising the navbar's responsive states, run a free Ritla scan: reports document-level horizontal overflow, and reports visible text-bearing elements overlapping. If the failure appears only as the header collapses, continue with RTL elements overlapping on mobile to trace the competing size constraints.