Skip to content

Right-to-Left

RTL here is load-bearing rather than a toggle that mostly works. This page says what mirrors, what does not, and why.

How it works

Switching direction sets dir="rtl" on <html> plus a dir-rtl class, and is restored before the first paint so there is no flash of the wrong direction. Everything else follows from using logical properties rather than physical ones.

ms-4  me-4      /* margin-inline-start / -end, not ml-4 / mr-4 */
ps-9  pe-4      /* padding-inline */
text-start      /* not text-left */
border-e        /* border-inline-end */
inset-inline-start
start-0  end-0

This is enforced, not merely recommended: ESLint bans the physical utilities (ml-, mr-, pl-, pr-, text-left, text-right, border-l, border-r) across the template, so a new component cannot quietly break mirroring.

What mirrors correctly

  • Every layout preset, including the pinned columns in the data grid and the floating rail's inset.
  • The sidebar, header, drawers, dropdowns, popovers and dialogs.
  • Forms, tables, the collapsed-rail chevron, and every icon whose direction carries meaning.
  • Progress bars, timelines and the divided stat row (which needs divide-x-reverse, since Tailwind's divide utilities are physical).

The accessibility suite scans the dashboard under RTL as well as in both themes, and the data grid's E2E tests assert a pinned column lands on the leading edge with the scroll origin on the right.

What does not mirror, and why

These are stated rather than quietly left broken. Every one is a deliberate decision.

  • The world map. Geography is not a layout. A mirrored world map is simply a wrong map, so the SVG never flips — only the legend and labels around it follow the writing direction.
  • A chart's secondary Y axis. Recharts lays out in pixels and its axis API accepts only "left" or "right" — there is no logical equivalent. Swapping the axes on direction change would move the data to a different scale, which is far more confusing than an axis on the unexpected side.
  • Horizontal bar charts. Laid out in pixels by the charting library, so a horizontal bar grows from the physical left regardless of direction. Fixable per chart — see the Traffic-by-source block, which flips it withreversed on the value axis and orientation on the category axis — but nothing in CSS can do it for you.
  • Code blocks. Source code is LTR content. A mirrored snippet is unreadable and uncopyable.

A related limit: locale hydration

Direction is restored before the first paint, but locale is not, and the two are often confused. This template is statically exported with a single prerendered locale, so a German or French visitor's first client render differs from the HTML in every text node. No pre-paint script can fix that — the text is not on <html>, it is the page.

The proper fix is a app/[locale]/… segment so each locale gets its own prerendered HTML. That is a structural change and is listed as future work rather than half-built.

Testing your own components

// Switch direction from the theme customizer, or:
localStorage.setItem("apex-direction", "rtl");
location.reload();

// In an E2E test:
await page.addInitScript(() => localStorage.setItem("apex-direction", "rtl"));

One thing to know when asserting positions: under RTL the scroll origin is on the right and scrollLeft goes negative. Measure from the container's leading edge rather than from its left.