Skip to content

RTL sidebars: placement, collapse and animation

Learn how to place, collapse, animate and resize an RTL sidebar while keeping content space, navigation labels and keyboard focus usable.

By , Founder of Ritla

Guide13 min read

Moving a sidebar to the right can expose a second bug: the page still reserves space on the left. Collapsing it may leave that empty strip behind, or remove the navigation visually while its links remain reachable with Tab. Placement, occupied space and interaction state need to change together.

For primary navigation at the reading start, an Arabic interface normally places the sidebar on the right. An inspector attached to a drawing canvas, a comparison panel or a user-positioned tool palette can have a different requirement. Define the panel's relationship to the content before deciding which edge should change with language.

This guide uses a museum conservation workspace. Its sidebar links to the collection, condition reports and treatment plans. The implementation keeps that navigation at inline start, releases its layout space when collapsed and preserves access to the reopening control. Optional resizing uses physical pointer movement converted into a change in sidebar width.

Decide what collapse means in this workspace#

The word “collapsed” can describe three different interfaces. They need different state and accessibility decisions.

StateWhat remains visibleWhat happens to the content area
Expanded navigationLabels and destinationsThe sidebar reserves its full width
Narrow navigation railRecognizable destination controlsThe rail still reserves some width
Dismissed navigationOnly an external reopening controlThe sidebar reserves no width

The worked example uses dismissal. It does not retain icons in a narrow strip. That lets the collapse control describe one operation and avoids pretending that labels clipped out of view form a usable rail.

If your design retains a rail, each destination still needs an accessible name and a visible way to identify an unfamiliar icon. A tooltip available only on pointer hover is insufficient for touch use. Treat a rail as another navigation presentation with its own width budget, rather than an expanded sidebar squeezed until its text disappears.

Keep the reopening control outside any region that becomes hidden or inert. Putting it inside the disappearing panel makes the next interaction depend on a keyboard shortcut, another navigation route or an accidental off-screen focus target. Its position should remain predictable in both languages.

Place the sidebar and reserve its space together#

A fixed-position sidebar and a separately offset main area create two representations of the same width. Both must agree on side, size and collapsed state. Here is the failure in a museum workspace originally written for English:

Problem

css
.collection-sidebar {  position: fixed;  left: 0;  width: 18rem;} .collection-main {  margin-left: 18rem;}

Changing only left to a right-side anchor moves the panel but leaves the content's reservation on the old side. Changing only the margin produces the opposite mismatch. Neither property follows direction automatically.

For a persistent panel that belongs beside the content, let the parent layout own both columns. The sidebar then occupies real layout space rather than requiring a manually synchronized margin. Use this markup for the following CSS and script; the example addresses are reserved illustrative URLs.

html
<div class="collection-app" lang="ar" dir="rtl">  <button id="collection-toggle" type="button"          aria-controls="collection-sidebar"          aria-expanded="true" hidden>    تنقل مساحة العمل  </button>   <div class="collection-shell" data-collapsed="false">    <div id="collection-sidebar" class="collection-sidebar">      <nav class="collection-sidebar__content"           aria-label="أقسام الحفظ والترميم">        <ul>          <li><a href="https://conservation.example/collection"                 aria-current="page">المجموعة</a></li>          <li><a href="https://conservation.example/condition">            تقارير الحالة          </a></li>          <li><a href="https://conservation.example/treatments">            خطط المعالجة          </a></li>        </ul>      </nav>    </div>     <main class="collection-main">      <h2>سجل المجموعة</h2>      <p>اختر قطعة للاطلاع على سجل الحفظ والترميم.</p>    </main>  </div></div>

The surrounding page supplies its usual heading hierarchy and toolbar styling. In a complete Arabic document, declare language and direction on the root and allow this component to inherit them. The local attributes make the standalone example's boundary explicit. Do not infer direction from the Arabic labels or apply dir="auto" to an entire application shell.

Better

css
.collection-shell {  --side-width: 18rem;  --side-track: var(--side-width);  display: grid;  grid-template-columns: var(--side-track) minmax(0, 3fr);  transition: grid-template-columns 180ms ease;} .collection-shell[data-collapsed="true"] {  --side-track: 0rem;} .collection-sidebar {  min-inline-size: 0;  overflow: clip;} .collection-sidebar__content {  box-sizing: border-box;  inline-size: var(--side-width);  padding-block: 1rem;  padding-inline: 1rem;  border-inline-end: 1px solid currentColor;} .collection-sidebar ul {  list-style: none;  margin: 0;  padding: 0;} .collection-sidebar a {  display: block;  padding-block: 0.75rem;  text-align: start;} .collection-main {  min-inline-size: 0;  padding: 1rem;} .collection-shell[data-collapsed="true"] .collection-sidebar {  visibility: hidden;}

The first grid column is at inline start. It is the right-hand column in horizontal Arabic and the left-hand column in English. Keeping the navigation first in source preserves its relationship to the main region without reversing the link list. MDN's grid and writing-mode guide explains this directional placement.

The 3fr track receives the remaining space because it is the only flexible track. Its zero minimum allows the main column to shrink rather than insisting on its contents' intrinsic minimum width. That does not resolve a wide table or canvas inside it; give such content its own appropriate scrolling region.

There is no column gap to survive after collapse. The visual separation comes from padding inside the two regions. If you add a grid gap, also decide what happens to it at zero sidebar width; an empty first track can otherwise leave a visible strip.

CSS geometry is only part of the state. The script must also update the opener's expanded value and make dismissed navigation unavailable for interaction. Run this after the markup exists, for example from a deferred script.

js
const shell = document.querySelector(".collection-shell");const sidebar = document.querySelector("#collection-sidebar");const toggle = document.querySelector("#collection-toggle"); function setSidebarClosed(closed) {  if (closed && sidebar.contains(document.activeElement)) {    toggle.focus();  }   sidebar.inert = closed;  shell.dataset.collapsed = String(closed);  toggle.setAttribute("aria-expanded", String(!closed));} toggle.hidden = false;toggle.addEventListener("click", () => {  setSidebarClosed(shell.dataset.collapsed !== "true");});setSidebarClosed(false);

Focus moves before the containing region becomes unavailable. Opening leaves focus on the toggle, allowing the next Tab movement to enter the navigation. Closing through the toggle already leaves focus there; the containment check also covers a future close action initiated from inside the sidebar.

The inert attribute prevents interaction with the subtree and removes it from the accessibility tree. visibility: hidden supplies the visual dismissal in this example. An opacity change alone would not provide the same behavior, and setting aria-hidden alone would not disable keyboard focus.

The initial markup exposes the links and hides the nonfunctional toggle. If JavaScript fails to load, navigation remains available. When integrating a saved collapse preference, first expose a usable toggle, then apply that preference through the state function. Do not server-render inaccessible navigation while relying on a later script to reveal its only opener.

This is ordinary website navigation, not an application menu widget. Retain native links and a button. Avoid positive tabindex values to imitate a particular screenshot order; make the document sequence meaningful and verify it with the actual layout.

Animate the space that actually changes#

In this implementation, the first grid track changes between two lengths while the second track keeps the same definition. The grid-template-columns reference documents animation between compatible track lists. Test the transition in your supported browsers; the state must remain usable if it changes instantly.

The inner navigation retains its expanded width. It does not progressively rewrap into narrower columns while the outer track contracts. Closing hides its contents immediately while the main region gains space; opening reveals them through the expanding clipped region. This is a deliberate reveal treatment, not a complete sliding drawer.

Clipping belongs to that transitional wrapper, not the entire page. Do not apply overflow: hidden to the document to conceal a misplaced sidebar. It can hide the evidence while leaving content unreachable. MDN's overflow reference also distinguishes clipping from a scroll container, which matters if you later add sticky behavior.

Respect a reduced-motion preference:

css
@media (prefers-reduced-motion: reduce) {  .collection-shell {    transition: none;  }}

The preference affects the transition, not whether navigation can be opened. MDN describes the media feature in its prefers-reduced-motion reference. State updates should not depend on receiving an animation-completion event, since this version does not need one.

Horizontal translations require a separate directional decision. A panel attached to the left exits outward with a negative physical offset; one attached to the right exits outward with a positive offset. That relationship follows the actual attachment edge, which may differ from the document's reading start for a secondary inspector.

Changing from transform: translateX(...) to the individual translate property does not make the horizontal axis logical. MDN's translate reference defines horizontal and vertical movement. If you use transforms for another sidebar design, specify each open and closed position before choosing the signs.

Keep narrow-screen mode separate from user preference#

An expanded desktop sidebar may leave too little space for the collection record at a narrow viewport. That is a layout constraint, not proof that the user changed their preference. Store “prefers expanded navigation” separately from “this viewport can accommodate a persistent sidebar.”

The simplest adaptation for this example is to stack the navigation above the main content and keep the same dismissal button:

css
@media (max-width: 52rem) {  .collection-shell {    display: block;    transition: none;  }   .collection-sidebar__content {    inline-size: auto;    border-inline-end: 0;    border-block-end: 1px solid currentColor;  }   .collection-shell[data-collapsed="true"] .collection-sidebar {    display: none;  }}

Here, resizing does not automatically alter the user's open state. Expanded navigation becomes an in-flow region above the record, and dismissed navigation remains dismissed. The 52rem threshold is a value to evaluate against this workspace's content, not a universal device category.

If the product instead requires an overlay on phones, define whether it is modal. A modal blocks interaction with the background and needs focus containment, dismissal and focus restoration. A nonmodal overlay must not claim modal semantics while the underlying record remains operable. The native dialog element, opened with showModal(), supplies browser modal behavior to build on.

Switching between a persistent sidebar and a modal is a state transition, not merely a media query. Decide what happens when the viewport widens while the modal is open, and when it narrows while a sidebar link has focus. Avoid maintaining two independently interactive copies of the navigation to sidestep those decisions.

Preserve the selected destination and expanded navigation groups when changing presentation. A record editor should not lose unsaved content because opening a sidebar reconstructs the main region. Test component mounting and routing behavior alongside the CSS.

Convert pointer movement into sidebar width#

An adjustable sidebar introduces physical coordinates even when its CSS uses logical properties. clientX increases toward the physical right. For a sidebar on the left, moving its inner boundary right makes it wider. For one on the right, moving the boundary left makes it wider.

The following helper converts that movement for an inline-start sidebar in a horizontal, unscaled workspace. It is resize arithmetic, not a complete draggable separator:

js
function sidebarWidth(startWidth, deltaX, direction, availableWidth) {  const minimum = 232;  const maximum = Math.min(416, availableWidth - 360);   if (maximum < minimum) return null;   const growth = direction === "rtl" ? -deltaX : deltaX;  return Math.max(minimum, Math.min(maximum, startWidth + growth));}

All measurements here are CSS pixels. startWidth is the expanded sidebar's width when dragging starts, deltaX is the pointer's current horizontal position minus its initial position, and availableWidth is the shell's usable width after any separately reserved gaps or controls. The main area must retain at least 360 pixels in this example.

For an RTL sidebar starting at 288 pixels, dragging 24 pixels left produces 312 pixels; dragging the same distance right produces 264. In LTR, the growth directions reverse. A null result means both minimum widths cannot fit. Switch to the compact presentation rather than storing an impossible width.

Read direction from the actual shell, not the browser language or a global translation setting. If placement intentionally uses inline end, the conversion must change accordingly. If the workspace is scaled with a transform, viewport movement and layout widths also require a coordinate conversion beyond this helper.

In an implementation, capture the start width and direction on pointer down, update the shell's --side-width with the resulting pixel value, and use the original start values throughout that drag. Adding the full accumulated delta to the previous move's width would compound the movement. Handle pointer cancellation and capture loss explicitly.

A draggable edge also needs keyboard operation and a perceivable focus target. The WAI window splitter pattern describes a focusable separator, its value and arrow movement, while noting that its example work is incomplete. Use it as interaction guidance and test your implementation with assistive technology. For a vertical splitter, moving left remains physical left; the corresponding change in sidebar width depends on which side owns the pane.

During a drag, disable the width transition so the layout follows the current pointer measurement rather than repeatedly starting an animation toward it. Re-enable the transition when the drag finishes or is cancelled. Keep the collapse transition and drag feedback as separate states; a single global “animating” flag makes interruptions harder to handle.

Do not start a resize by measuring the dismissed sidebar. Its outer track is zero, which is the hidden layout state rather than the user's expanded width. Restore or read the expanded size first, then begin the interaction from that size. The same distinction matters when implementing an expand action on the separator itself.

Restore a width preference within today's limits#

Save the expanded width and preferred open state as separate values. Zero should mean dismissed in the current layout, not become the next expanded width. Otherwise reopening can produce an apparently empty sidebar even though its state and expanded announcement say it is available.

Validate any stored value before using it as a CSS length. Accept a finite numeric width in the expected unit, then constrain it against the current shell and main-content requirements. A width selected on a large monitor may leave no usable record area after the same workspace is restored on a smaller screen.

Decide whether constraints should overwrite the preference or merely limit its current presentation. Keeping the requested width lets the interface restore it when more space becomes available. Saving the temporary constrained width instead changes that preference. Either policy can be intentional, but it should not depend on whichever resize event runs last.

Changing locale should preserve a width preference without preserving a physical edge accidentally. This workspace's sidebar remains at inline start; the width is reusable, while its attachment edge follows direction. If the product also supports user-selected panel placement, store that explicit choice separately and apply the matching resize conversion.

Keep long navigation and the current item reachable#

Width is not the only constraint. A conservation workspace may contain nested collection sections, saved views and permission-dependent destinations. Test the longest open navigation group at a short viewport height, not just the default three links.

Choose whether the document scrolls as a whole or the sidebar has its own scrolling region. If the sidebar scrolls independently, its available height must come from the actual application shell. An assumed full viewport height can put the last link behind a persistent header or toolbar. Check the bottom destination at browser zoom as well as at the default text size.

The animation wrapper above clips overflow. It is not a long-menu scrolling solution. If independent scrolling is required, add it to a suitably height-constrained inner region and test focus outlines at that region's edges. Do not indiscriminately replace clipping with scrolling on every ancestor; inspect which element is intended to own the scroll.

Use an approved long label such as القطع التي تنتظر مراجعة خطة المعالجة to check wrapping. Keep the destination distinguishable from nearby entries. Ellipsizing every label may preserve a tidy column while making several destinations look identical. If truncation is a deliberate choice, provide another way to obtain the complete label.

Mark the active destination independently of expansion state. A collapsed group and a current route answer different questions. On reopening, verify that the selected destination remains identifiable and can be reached without resetting unrelated groups or the main record's scroll position.

Clipping can also affect a rail's explanatory labels or a nested submenu. Before adding a tooltip or popup, inspect whether it must extend beyond the sidebar wrapper. A layer rendered elsewhere in the document needs its own direction and positioning context; changing its z-index does not resolve every clipping relationship. Test the rendered layer at the boundary between the sidebar and main region.

Choose sticky or fixed behavior deliberately#

A persistent sidebar is not necessarily fixed to the viewport. The grid example participates in document layout and scrolls with the page. Add stickiness only when the navigation needs to remain available during a long record, and identify the intended scroll container first.

For a sticky design, inspect the ancestors' overflow rules and the available vertical space. A panel taller than the usable viewport needs a scrolling strategy, not just a sticky top offset. Include the application toolbar in that calculation, and repeat it after zoom or translated toolbar labels change the occupied height. The CSS position reference explains sticky positioning relative to scrolling ancestors.

A genuinely fixed sidebar is outside normal flow, so its content reservation must be maintained explicitly. Use the same width source for the panel and the main region's logical offset, and reset both on dismissal. Check for inherited margins from the previous implementation before adding another offset; two individually plausible reservations can double the empty space.

Fixing the panel can also change how its popups interact with surrounding layers. Inspect the actual containing block and stacking context before raising z-index values. Compare the open submenu, the record toolbar and any modal dialog in the states where they coexist. The sidebar's side, its occupied space and its layer order are independent decisions.

Test transitions from both sides of the layout#

Create Arabic and English fixtures with the same destination order and permission state. Inspect the grid tracks, sidebar wrapper and main region separately. A panel in the right place does not establish that its reserved width or inner divider is correct.

Use these transition tests:

  1. Expand and dismiss in both locales. Confirm that the main region receives the released space and no empty gap remains at the old edge.
  2. Focus a sidebar link, then invoke the close function through the application's close action. Confirm that focus reaches the visible reopening button before the navigation becomes unavailable.
  3. Tab through the dismissed state. Check that no sidebar links receive focus, then reopen and verify their order and accessible names.
  4. Resize across the compact threshold with navigation both open and closed. Check state preservation, the current destination and any modal cleanup required by your chosen presentation.
  5. Exercise the longest group at a short height and a narrow width. Reach the last destination with a keyboard and inspect focus visibility near clipped or scrolling edges.
  6. If resizing is supported, drag outward and inward in each locale, reach both limits and cancel a drag. Repeat with the keyboard, then change locale and restore a saved width.
  7. Enable reduced motion and repeat rapid toggling. The final interactive state must agree with the visual state even without a transition.

For a positioning failure, use getBoundingClientRect() to inspect the sidebar's viewport bounds. Check both physical edges against the shell's intended bounds. A document scroll-width comparison alone does not prove that nothing has escaped the viewport, especially when RTL content overhangs the side that does not extend the scrollable area.

Repeat the tests in the browsers and assistive-technology combinations the product supports. Save failures with the locale, viewport, open state, width preference and transition that produced them; a screenshot of the final sidebar cannot explain a lost-focus bug.

Run a free Ritla scan on the sidebar layout to check for document-level horizontal overflow with R040; R033 concerns translateX moving the element off-screen in RTL. If the main record still reserves space at the wrong edge, use physical versus logical CSS to trace the remaining offsets, or RTL CSS grid to inspect the parent tracks.

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