---
id: "packages/active-i18n/direction"
title: "Direction"
description: "Right-to-left languages - who sets the direction, and how the components and formats follow it."
url: "/packages/active-i18n/direction"
locale: "en"
revision: "fff18e39989e4fa9"
---

# Direction

**Example: Language, formats and reading direction**

Client: Runs entirely in your browser. Source: `examples/i18n/direction.tsx`.

```tsx
// apps/docs/examples/i18n/direction.tsx
'use client';

import {useState} from 'react';

import {toIanaTimeZone} from '@workspace/active-format';
import type {LanguageConfig, LocaleTranslations} from '@workspace/active-i18n';
import {
  fixedTimeZone,
  I18nProvider,
  TimeZoneProvider,
  useFormatters,
  useLocale,
  useTranslations,
} from '@workspace/active-i18n/react';
import {DirectionProvider} from '@workspace/active-ui/components/primitives/direction';
import {Slider} from '@workspace/active-ui/components/primitives/slider';
import {
  Tabs,
  TabsList,
  TabsTrigger,
} from '@workspace/active-ui/components/primitives/tabs';

// The host's language set: the direction of each language is the host's data.
const LANGUAGES: readonly LanguageConfig[] = [
  {code: 'en', label: 'English', direction: 'ltr'},
  {code: 'ar', label: 'العربية', direction: 'rtl'},
];

const TRANSLATIONS: Record<string, LocaleTranslations> = {
  en: {
    order: {
      title: 'Order {{id}}',
      placed: 'Placed on',
      total: 'Total',
      progress: 'Progress',
    },
  },
  ar: {
    order: {
      title: 'الطلب {{id}}',
      placed: 'تاريخ الطلب',
      total: 'المجموع',
      progress: 'التقدم',
    },
  },
};

function OrderSummary() {
  const t = useTranslations();
  const locale = useLocale();
  const format = useFormatters();
  return (
    <div className="flex flex-col gap-3">
      <p className="font-medium" data-i18n-locale={locale}>
        {t('order.title', {id: '1042'})}
      </p>
      <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
        <dt className="text-muted-foreground">{t('order.placed')}</dt>
        <dd>{format.dateTime('2026-10-21T09:30:00Z')}</dd>
        <dt className="text-muted-foreground">{t('order.total')}</dt>
        <dd>{format.currency({amount: 1234.5, currency: 'EUR'})}</dd>
      </dl>
      <Slider defaultValue={30} aria-label={t('order.progress')} />
    </div>
  );
}

/**
 * The language decides the words and the formats; the host decides the
 * direction from its language data and sets `dir` and `DirectionProvider`,
 * so the layout and the arrow keys follow the reading direction.
 */
export default function Direction() {
  const [code, setCode] = useState('ar');
  const language = LANGUAGES.find((entry) => entry.code === code)!;
  const direction = language.direction ?? 'ltr';
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <Tabs value={code} onValueChange={(value) => setCode(String(value))}>
        <TabsList aria-label="Language">
          {LANGUAGES.map((entry) => (
            <TabsTrigger key={entry.code} value={entry.code}>
              {entry.label}
            </TabsTrigger>
          ))}
        </TabsList>
      </Tabs>
      {/* A real host navigates to the other language (setLocale); this
          demonstration remounts the provider instead. */}
      <I18nProvider
        key={code}
        locale={code}
        translations={TRANSLATIONS[code]!}
        supportedLanguages={LANGUAGES}
        allowLocaleSwitching={false}
      >
        <TimeZoneProvider
          value={fixedTimeZone(toIanaTimeZone('Europe/Amsterdam'))}
        >
          <DirectionProvider direction={direction}>
            <div
              lang={code}
              dir={direction}
              className="rounded-lg border p-4"
              data-i18n-direction={direction}
            >
              <OrderSummary />
            </div>
          </DirectionProvider>
        </TimeZoneProvider>
      </I18nProvider>
    </div>
  );
}
```

## Who decides the direction

The direction of a language is **your data**: `LanguageConfig.direction` (`'ltr'` or `'rtl'`)
carries it with your language set. The package does not set `dir` anywhere. Your layout does, on
`<html>` for a whole page (or on the region that shows another language), together with active-ui's
`DirectionProvider`, so that components whose keys depend on direction (tabs, sliders, menus) turn
around too:

```tsx
import {DirectionProvider} from '@workspace/active-ui/components/primitives/direction';

const direction = language.direction ?? 'ltr';

<html lang={language.code} dir={direction}>
  <body>
    <DirectionProvider direction={direction}>{children}</DirectionProvider>
  </body>
</html>;
```

## What follows the language

- **Words** come from your dictionary for that language.
- **Formats** come from the presentation: `useFormatters()` formats dates, numbers and amounts for
  the provider's language, with the date order, separators and digits the runtime's CLDR data gives that language.
- **Layout** follows `dir`: logical CSS properties (`margin-inline-start`, `ps-*`) turn around;
  physical ones (`left`) do not.

A grid and a chart take their direction from their own `dir` prop or the page, never from the
locale, so the same rule holds there.
