Skip to main content

Overview

Radix UI Primitives are built with internationalization in mind, providing automatic support for right-to-left (RTL) languages and direction-aware interactions.
Components automatically adapt keyboard navigation, positioning, and animations based on the reading direction of your application.

Reading Direction (RTL/LTR)

Reading direction affects how users interact with components. Radix components adapt automatically.

The Direction Provider

Wrap your application with DirectionProvider to set the reading direction:
import { DirectionProvider } from '@radix-ui/react-direction';
import * as Accordion from '@radix-ui/react-accordion';

function App() {
  return (
    <DirectionProvider dir="rtl">
      <Accordion.Root type="single" collapsible>
        {/* Components automatically adapt to RTL */}
      </Accordion.Root>
    </DirectionProvider>
  );
}
From packages/react/direction/src/direction.tsx:14:
const DirectionProvider: React.FC<DirectionProviderProps> = (props) => {
  const { dir, children } = props;
  return <DirectionContext.Provider value={dir}>{children}</DirectionContext.Provider>;
};

Supported Values

  • "ltr" - Left-to-right (English, Spanish, French, etc.)
  • "rtl" - Right-to-left (Arabic, Hebrew, Persian, etc.)

Using the Direction Hook

Components use the useDirection hook internally to adapt behavior:
import { useDirection } from '@radix-ui/react-direction';

function MyComponent({ dir }) {
  const direction = useDirection(dir);
  // direction is 'ltr' or 'rtl'
  
  return (
    <div style={{ textAlign: direction === 'rtl' ? 'right' : 'left' }}>
      Content
    </div>
  );
}
From packages/react/direction/src/direction.tsx:21:
function useDirection(localDir?: Direction) {
  const globalDir = React.useContext(DirectionContext);
  return localDir || globalDir || 'ltr';
}
The hook respects component-level dir props over global context, and defaults to "ltr" if neither is provided.

Direction-Aware Keyboard Navigation

Components automatically adjust keyboard navigation based on reading direction.

Example: Accordion

In packages/react/accordion/src/accordion.tsx:230, the Accordion component adapts arrow key behavior:
const AccordionImpl = React.forwardRef<AccordionImplElement, AccordionImplProps>(
  (props, forwardedRef) => {
    const { dir, orientation = 'vertical', ...accordionProps } = props;
    const direction = useDirection(dir);
    const isDirectionLTR = direction === 'ltr';

    const handleKeyDown = composeEventHandlers(props.onKeyDown, (event) => {
      // ... trigger collection logic ...

      switch (event.key) {
        case 'ArrowRight':
          if (orientation === 'horizontal') {
            if (isDirectionLTR) {
              moveNext(); // LTR: right = next
            } else {
              movePrev(); // RTL: right = previous
            }
          }
          break;
        case 'ArrowLeft':
          if (orientation === 'horizontal') {
            if (isDirectionLTR) {
              movePrev(); // LTR: left = previous
            } else {
              moveNext(); // RTL: left = next
            }
          }
          break;
        // ...
      }
    });

    return (
      <Primitive.div
        {...accordionProps}
        data-orientation={orientation}
        ref={composedRefs}
        onKeyDown={disabled ? undefined : handleKeyDown}
      />
    );
  },
);

What Gets Adapted

LTR (left-to-right):
  • Arrow Right → Move forward/next
  • Arrow Left → Move backward/previous
RTL (right-to-left):
  • Arrow Right → Move backward/previous
  • Arrow Left → Move forward/next
This happens automatically. You don’t need to write conditional logic for RTL support.

Per-Component Direction

You can override the global direction for individual components:
import { DirectionProvider } from '@radix-ui/react-direction';
import * as Accordion from '@radix-ui/react-accordion';

function App() {
  return (
    <DirectionProvider dir="ltr">
      <div>
        {/* This accordion uses LTR (from provider) */}
        <Accordion.Root type="single" collapsible>
          {/* ... */}
        </Accordion.Root>

        {/* This accordion overrides to RTL */}
        <Accordion.Root type="single" collapsible dir="rtl">
          {/* ... */}
        </Accordion.Root>
      </div>
    </DirectionProvider>
  );
}

Components Supporting dir Prop

These components accept a dir prop:
  • Accordion
  • ContextMenu
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Select
  • Tooltip
Check component documentation for specifics.

Styling for RTL

Logical Properties

Use CSS logical properties for automatic RTL support:
/* Bad: Fixed directional properties */
.element {
  margin-left: 16px;
  padding-right: 8px;
  border-left: 1px solid gray;
}

/* Good: Logical properties */
.element {
  margin-inline-start: 16px;
  padding-inline-end: 8px;
  border-inline-start: 1px solid gray;
}
Logical property mappings:
PhysicalLogicalLTRRTL
margin-leftmargin-inline-startleftright
margin-rightmargin-inline-endrightleft
padding-leftpadding-inline-startleftright
padding-rightpadding-inline-endrightleft
leftinset-inline-startleftright
rightinset-inline-endrightleft

Direction Attribute Selectors

Use [dir] attribute in CSS:
/* LTR-specific styles */
[dir="ltr"] .element {
  text-align: left;
}

/* RTL-specific styles */
[dir="rtl"] .element {
  text-align: right;
}

Transforms and Animations

Flip transforms for RTL:
.icon {
  transition: transform 200ms;
}

[dir="ltr"] .icon[data-state="open"] {
  transform: rotate(180deg);
}

[dir="rtl"] .icon[data-state="open"] {
  transform: rotate(-180deg);
}

Tailwind CSS

Tailwind supports RTL with the rtl: modifier:
<div className="ml-4 rtl:mr-4 rtl:ml-0">
  Content
</div>
Or enable RTL mode globally:
// tailwind.config.js
module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require('tailwindcss-rtl'),
  ],
}

Complete RTL Example

Here’s a fully RTL-aware component:
import { DirectionProvider } from '@radix-ui/react-direction';
import * as Accordion from '@radix-ui/react-accordion';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import './styles.css';

function App() {
  const [locale, setLocale] = useState('en');
  const dir = locale === 'ar' ? 'rtl' : 'ltr';

  return (
    <DirectionProvider dir={dir}>
      <div dir={dir}>
        <header>
          <select value={locale} onChange={(e) => setLocale(e.target.value)}>
            <option value="en">English</option>
            <option value="ar">العربية</option>
            <option value="he">עברית</option>
          </select>
        </header>

        <Accordion.Root type="single" collapsible className="accordion">
          <Accordion.Item value="item-1" className="accordion-item">
            <Accordion.Header>
              <Accordion.Trigger className="accordion-trigger">
                <span>{locale === 'ar' ? 'هل يمكن الوصول إليه؟' : 'Is it accessible?'}</span>
                <ChevronDownIcon className="accordion-chevron" aria-hidden />
              </Accordion.Trigger>
            </Accordion.Header>
            <Accordion.Content className="accordion-content">
              {locale === 'ar'
                ? 'نعم. إنه يلتزم بأنماط تصميم WAI-ARIA.'
                : 'Yes. It adheres to the WAI-ARIA design patterns.'}
            </Accordion.Content>
          </Accordion.Item>
        </Accordion.Root>
      </div>
    </DirectionProvider>
  );
}
With RTL-aware CSS:
/* styles.css */
.accordion {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
}

.accordion-item {
  border-bottom: 1px solid #e5e7eb;
}

.accordion-item:last-child {
  border-bottom: none;
}

.accordion-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
  width: 100%;
  background: transparent;
  border: none;
  cursor: pointer;
}

.accordion-trigger:hover {
  background-color: #f9fafb;
}

.accordion-chevron {
  transition: transform 200ms;
}

[dir="ltr"] .accordion-chevron[data-state="open"] {
  transform: rotate(180deg);
}

[dir="rtl"] .accordion-chevron[data-state="open"] {
  transform: rotate(-180deg);
}

.accordion-content {
  padding: 16px;
  /* Use logical properties */
  padding-inline-start: 24px;
  padding-inline-end: 24px;
}

/* Animation */
@keyframes slideDown {
  from {
    height: 0;
  }
  to {
    height: var(--radix-accordion-content-height);
  }
}

.accordion-content[data-state="open"] {
  animation: slideDown 300ms ease-out;
}

Locale Considerations

Text Alignment

Match text alignment to reading direction:
import { useDirection } from '@radix-ui/react-direction';

function LocalizedText({ children }) {
  const direction = useDirection();
  
  return (
    <p style={{ textAlign: direction === 'rtl' ? 'right' : 'left' }}>
      {children}
    </p>
  );
}

Date and Number Formatting

Use Intl APIs for locale-aware formatting:
function LocalizedDate({ date, locale }) {
  const formatted = new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date);

  return <time>{formatted}</time>;
}

// Usage
<LocalizedDate date={new Date()} locale="ar-EG" />
// Output: ٤ مارس ٢٠٢٦

String Translation

Integrate with i18n libraries:
import { useTranslation } from 'react-i18next';
import * as Dialog from '@radix-ui/react-dialog';

function LocalizedDialog() {
  const { t, i18n } = useTranslation();
  const dir = i18n.dir(); // 'ltr' or 'rtl'

  return (
    <DirectionProvider dir={dir}>
      <Dialog.Root>
        <Dialog.Trigger>{t('dialog.open')}</Dialog.Trigger>
        <Dialog.Portal>
          <Dialog.Overlay />
          <Dialog.Content>
            <Dialog.Title>{t('dialog.title')}</Dialog.Title>
            <Dialog.Description>{t('dialog.description')}</Dialog.Description>
            <Dialog.Close>{t('dialog.close')}</Dialog.Close>
          </Dialog.Content>
        </Dialog.Portal>
      </Dialog.Root>
    </DirectionProvider>
  );
}

HTML Direction Attribute

Always set the dir attribute on your HTML:
function App() {
  const [locale, setLocale] = useState('en');
  const dir = ['ar', 'he', 'fa'].includes(locale) ? 'rtl' : 'ltr';

  useEffect(() => {
    document.documentElement.dir = dir;
  }, [dir]);

  return (
    <DirectionProvider dir={dir}>
      {/* App content */}
    </DirectionProvider>
  );
}
Or in your HTML template:
<!DOCTYPE html>
<html dir="ltr" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Setting dir on the <html> element ensures the entire document respects the reading direction, including browser UI like scrollbars.

Testing RTL Support

Manual Testing

  1. Add the DirectionProvider with dir="rtl"
  2. Verify visual layout mirrors correctly
  3. Test keyboard navigation (arrow keys)
  4. Check text alignment and spacing
  5. Test on actual RTL content (Arabic, Hebrew)

Automated Testing

import { render, screen } from '@testing-library/react';
import { DirectionProvider } from '@radix-ui/react-direction';
import * as Accordion from '@radix-ui/react-accordion';

test('accordion adapts to RTL', () => {
  render(
    <DirectionProvider dir="rtl">
      <Accordion.Root type="single" collapsible>
        <Accordion.Item value="item-1">
          <Accordion.Header>
            <Accordion.Trigger>Trigger</Accordion.Trigger>
          </Accordion.Header>
          <Accordion.Content>Content</Accordion.Content>
        </Accordion.Item>
      </Accordion.Root>
    </DirectionProvider>
  );

  // Test that component received RTL context
  // (implementation-specific tests)
});

Common RTL Pitfalls

1. Hardcoded Directional Styles

Avoid:
.element {
  margin-left: 16px; /* Won't flip in RTL */
}
Use:
.element {
  margin-inline-start: 16px; /* Automatically flips */
}

2. Missing dir Attribute

Avoid:
<DirectionProvider dir="rtl">
  {/* Missing dir on DOM */}
  <div>
    <Accordion.Root>{/* ... */}</Accordion.Root>
  </div>
</DirectionProvider>
Include:
<DirectionProvider dir="rtl">
  <div dir="rtl">
    <Accordion.Root>{/* ... */}</Accordion.Root>
  </div>
</DirectionProvider>

3. Asymmetric Layouts

Be careful with custom layouts that assume LTR: Avoid:
<div style={{ paddingLeft: 40, paddingRight: 16 }}>
  {/* Won't mirror correctly */}
</div>
Use:
<div style={{ paddingInlineStart: 40, paddingInlineEnd: 16 }}>
  {/* Mirrors correctly */}
</div>

4. Icons and Images

Flip directional icons in RTL:
import { ChevronRightIcon } from '@radix-ui/react-icons';
import { useDirection } from '@radix-ui/react-direction';

function DirectionalChevron() {
  const direction = useDirection();
  
  return (
    <ChevronRightIcon
      style={{
        transform: direction === 'rtl' ? 'scaleX(-1)' : undefined,
      }}
    />
  );
}

Browser Support

RTL features are well-supported:
  • CSS logical properties: All modern browsers
  • dir attribute: All browsers
  • DirectionProvider: Works everywhere React works
For older browsers, consider adding a PostCSS plugin to transform logical properties to physical ones with [dir] selectors.

Summary

Radix UI internationalization provides:
  • Automatic RTL support via DirectionProvider
  • Direction-aware keyboard navigation
  • Per-component direction overrides
  • Works with any i18n library
  • No extra configuration needed
Just wrap your app with DirectionProvider and Radix components automatically adapt to the reading direction.

Build docs developers (and LLMs) love