How to support right-to-left (RTL) languages in TanStack Start v1

Mirror layouts for Arabic and Hebrew

Problem

Most web layouts assume text flows from left to right. Navigation menus anchor to the left, sidebars appear on the left, and content reads left to right. For languages like Arabic and Hebrew that read right to left, this creates a disorienting experience where the visual flow contradicts the reading direction. Users see navigation on the wrong side, icons positioned awkwardly, and layouts that feel backward. Without proper RTL support, the interface becomes difficult to navigate and understand for RTL language speakers.

Solution

Set the document's text direction dynamically based on the current locale, allowing the browser to automatically mirror the layout for RTL languages. Use CSS logical properties instead of physical directional properties so that spacing, positioning, and alignment adapt to the text direction without additional code. This approach lets layouts remain direction-agnostic: what appears at the "start" of content in English will correctly appear at the "start" (right side) in Arabic, with the browser handling the transformation.

Steps

1. Determine text direction from locale

Create a helper function that returns the text direction for a given locale using the browser's built-in internationalization API.

export function getTextDirection(locale: string): "ltr" | "rtl" {
  try {
    const localeObj = new Intl.Locale(locale);
    if (
      "getTextInfo" in localeObj &&
      typeof localeObj.getTextInfo === "function"
    ) {
      return localeObj.getTextInfo().direction;
    }
  } catch (e) {
    console.warn(`Could not determine direction for locale: ${locale}`);
  }

  const rtlLocales = ["ar", "he", "fa", "ur"];
  const lang = locale.split("-")[0];
  return rtlLocales.includes(lang) ? "rtl" : "ltr";
}

This function uses Intl.Locale.getTextInfo() when available and falls back to a list of known RTL languages. It returns either 'ltr' or 'rtl' based on the locale.

2. Set the dir attribute on the html element

In your root route, retrieve the current locale and apply the corresponding direction to the document's <html> element.

import {
  createRootRoute,
  Outlet,
  Scripts,
  HeadContent,
} from "@tanstack/react-router";
import { useIntl } from "react-intl";
import { getTextDirection } from "~/utils/text-direction";

export const Route = createRootRoute({
  component: RootComponent,
});

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  );
}

function RootDocument({ children }: { children: React.ReactNode }) {
  const intl = useIntl();
  const dir = getTextDirection(intl.locale);

  return (
    <html lang={intl.locale} dir={dir}>
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  );
}

The dir attribute on the <html> element tells the browser to reverse the layout for RTL languages. Flexbox, grid, and inline content automatically mirror.

3. Replace physical CSS properties with logical properties

Update your stylesheets to use logical properties that respond to text direction instead of fixed physical directions.

.sidebar {
  padding-inline-start: 1rem;
  margin-inline-end: 2rem;
  border-inline-start: 1px solid #ccc;
}

.icon {
  margin-inline-end: 0.5rem;
}

.card {
  inset-inline-start: 0;
  text-align: start;
}

Logical properties like padding-inline-start map to padding-left in LTR and padding-right in RTL. The browser applies the correct physical property based on the dir attribute, so your styles work in both directions without duplication.

4. Use direction-agnostic alignment values

Replace physical alignment keywords with logical ones in your CSS and inline styles.

export function Header() {
  return (
    <header
      style={{
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      }}
    >
      <nav style={{ display: "flex", gap: "1rem" }}>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
      <div style={{ textAlign: "end" }}>
        <button>Menu</button>
      </div>
    </header>
  );
}

Using textAlign: 'end' instead of 'right' ensures text aligns to the end of the reading direction. Flexbox properties like justifyContent and alignItems automatically respect the direction set by the dir attribute.