JSPM

expo-rtl

1.0.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 15
  • Score
    100M100P100Q71905F
  • License MIT

RTL (right-to-left) support for Expo & React Native — per-component layout flipping, Arabic/Hebrew i18n, NativeWind support, no restart required

Package Exports

  • expo-rtl
  • expo-rtl/i18n
  • expo-rtl/nativewind
  • expo-rtl/package.json

Readme

expo-rtl

RTL (right-to-left) support for Expo and React Native — per-component, no restart required.

Add Arabic, Hebrew, and other RTL language support to your Expo or React Native app without restarting. Unlike I18nManager.forceRTL(), expo-rtl flips layout per component using React context — so you can mix LTR and RTL in the same screen.

22 drop-in replacements for React Native components (View, Text, FlatList, ScrollView, etc.) that automatically flip styles, margins, padding, borders, transforms, and text direction. Works with NativeWind / Tailwind CSS out of the box.

Includes a full i18n system: translations with interpolation and pluralization, locale auto-detection, AsyncStorage persistence, fallback chains (ar-EG → ar → en), async loading, and locale-aware formatNumber / formatDate.

Demo

Layout Flipping i18n & Translations RTL Animations
Layout Demo i18n Demo Animation Demo
Styles, margins, and flex direction flip automatically Switch locale live — Arabic, Hebrew, English translateX and scaleX auto-negate in RTL

Why expo-rtl instead of I18nManager?

I18nManager.forceRTL() expo-rtl
Granularity App-wide only Per-component (dir prop)
Restart required Yes No
Mix LTR + RTL Not possible Nested direction overrides
NativeWind support Manual Automatic
i18n built-in No Translations, pluralization, formatting
Style flipping Partial (only logical props) All physical + logical + transforms

Features

  • Drop-in componentsView, Text, TextInput, ScrollView, FlatList, Image, and 16 more
  • Per-component direction — flip individual subtrees with the dir prop, no global restart
  • Style flipping — margins, padding, borders, border-radius, absolute positioning, flexDirection, textAlign, alignItems/alignSelf, transforms, logical properties (start/end)
  • noFlip opt-out — keep specific components LTR even inside an RTL subtree
  • Image mirroringflip prop for directional icons (arrows, chevrons)
  • Built-in i18n — translations with {{var}} interpolation, pluralization (_one/_other), nested dot-notation keys
  • Locale auto-detection — reads device locale via expo-localization (optional)
  • Locale persistence — saves locale choice to AsyncStorage (optional)
  • Locale fallback chain — regional fallbacks like ar-EG → ar → en auto-expanded from subtags
  • Async translation loading — lazy-load translations per locale via loadTranslations
  • Locale-aware formattingformatNumber() and formatDate() powered by Intl
  • Type-safe translations — autocomplete on t() keys via TranslationKeys<typeof translations>
  • NativeWind support — optional className interop with full RTL flipping
  • Animated components — factory for RTL-aware animated components with auto-negated translateX/scaleX

Installation

npm install expo-rtl

Peer dependencies:

npm install react react-native
# Optional:
npm install react-native-safe-area-context  # for SafeAreaView
npm install expo-localization               # for auto-detecting device locale
npm install @react-native-async-storage/async-storage  # for persisting locale
npm install nativewind react-native-css-interop  # for NativeWind v4

Quick Start

1. Wrap your app

import { RTLProvider } from "expo-rtl";

const translations = {
  en: {
    greeting: "Hello {{name}}!",
    items_one: "{{count}} item",
    items_other: "{{count}} items",
    settings: { title: "Settings", profile: "Profile" },
  },
  ar: {
    greeting: "مرحبا {{name}}!",
    items_one: "عنصر {{count}}",
    items_other: "{{count}} عناصر",
    settings: { title: "الإعدادات", profile: "الملف الشخصي" },
  },
};

export default function App() {
  return (
    <RTLProvider defaultLocale="en" fallbackLocale="en" persistLocale translations={translations}>
      <MyApp />
    </RTLProvider>
  );
}

2. Use components and hooks

import { View, Text, Image, useRTL } from "expo-rtl";

function MyScreen() {
  const { t, direction, locale, setLocale, formatNumber } = useRTL();

  return (
    <View style={{ flexDirection: "row", gap: 12, paddingLeft: 16 }}>
      <Image
        flip
        source={require("./arrow-right.png")}
        style={{ width: 24, height: 24 }}
      />
      <Text>{t("greeting", { name: "Ali" })}</Text>
      <Text>{t("items", { count: 5 })}</Text>
      <Text>{formatNumber(1234.56)}</Text>
    </View>
  );
}

When locale is "ar", the row reverses, paddingLeft moves to the right, the arrow image mirrors, and all text renders in Arabic.

Components

All components are drop-in replacements for their React Native equivalents. They accept all original props plus:

Prop Type Description
dir "ltr" | "rtl" | "auto" Override direction for this component and its children
noFlip boolean Opt out of style flipping (children still flip)
className string NativeWind class name (requires expo-rtl/nativewind import)

Available components (22):

View, Text, TextInput, ScrollView, FlatList, SectionList, VirtualizedList, Pressable, TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, TouchableNativeFeedback, Image, ImageBackground, SafeAreaView, KeyboardAvoidingView, Modal, Switch, ActivityIndicator, Button, DrawerLayoutAndroid, RefreshControl

Special components

Image / ImageBackground — has an additional flip prop:

// Mirrors the image horizontally in RTL (useful for arrows, chevrons)
<Image flip source={require("./chevron-right.png")} />

DrawerLayoutAndroid — automatically flips drawerPosition (leftright) in RTL.

Button — wrapped for direction context participation (no style prop on RN Button).

Animated Components

Use createRTLAnimatedComponent() to wrap any animated component. It auto-negates translateX and scaleX in RTL, including Animated.Value objects:

import { createRTLAnimatedComponent } from "expo-rtl";
import { Animated } from "react-native";

const RTLAnimatedView = createRTLAnimatedComponent(Animated.View);

function SlideIn() {
  const slideX = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    // translateX: 200 slides right in LTR, left in RTL — automatically
    Animated.spring(slideX, { toValue: 200, useNativeDriver: true }).start();
  }, []);

  return (
    <RTLAnimatedView style={{ transform: [{ translateX: slideX }] }}>
      <Text>Slides the correct direction</Text>
    </RTLAnimatedView>
  );
}

ScrollView / FlatList

Horizontal scroll components automatically start from the correct side in RTL. contentContainerStyle is also flipped.

Automatic Style Flipping for RTL

When direction is RTL, these style properties are automatically transformed:

Category What flips
Physical properties marginLeftmarginRight, paddingLeftpaddingRight, borderLeftWidthborderRightWidth, borderLeftColorborderRightColor, leftright
Logical properties marginStartmarginEnd, paddingStartpaddingEnd, borderStartWidthborderEndWidth, borderStartColorborderEndColor, startend
Border radius (physical) borderTopLeftRadiusborderTopRightRadius, borderBottomLeftRadiusborderBottomRightRadius
Border radius (logical) borderTopStartRadiusborderTopEndRadius, borderBottomStartRadiusborderBottomEndRadius
Values flexDirection: "row""row-reverse", textAlign: "left""right" / "start""end"
Alignment alignItems/alignSelf: "flex-start""flex-end" (only in vertical layouts)
Transforms translateX and scaleX are negated (supports Animated.Value)
Text defaults Text and TextInput get direction-aware textAlign and writingDirection automatically

Note on logical properties: React Native resolves marginStart/marginEnd etc. via I18nManager, which expo-rtl does not use. So these properties are swapped by expo-rtl to ensure correct behavior in per-component RTL mode. This is important for NativeWind classes like ms-*, me-*, ps-*, pe-*, start-*, end-*.

RTLProvider

The provider supports controlled and uncontrolled modes:

// Uncontrolled — library manages locale state (recommended)
<RTLProvider defaultLocale="en" fallbackLocale="en" translations={translations}>

// With auto-detection (no defaultLocale — reads device locale via expo-localization)
<RTLProvider fallbackLocale="en" translations={translations}>

// With persistence (saves locale to AsyncStorage, restores on restart)
<RTLProvider defaultLocale="en" persistLocale translations={translations}>

// Controlled — you manage locale state externally
<RTLProvider locale={myLocale} translations={translations}>

// With async translation loading
<RTLProvider
  defaultLocale="en"
  loadTranslations={(locale) => fetch(`/i18n/${locale}.json`).then(r => r.json())}
  loadingFallback={<LoadingScreen />}
>
Prop Type Description
defaultLocale string Initial locale (uncontrolled mode). If omitted, auto-detects from device.
locale string Controlled locale
fallbackLocale string | string[] Fallback when a key is missing. Supports chain: ["fr", "en"]
translations Translations Static translation strings keyed by locale
loadTranslations (locale: string) => Promise<TranslationMap> Async loader for translations (cached per locale)
loadingFallback ReactNode Rendered while async translations load
persistLocale boolean Save/restore locale from AsyncStorage

Locale Auto-Detection

When no locale or defaultLocale is provided, the library auto-detects the device locale via expo-localization. If expo-localization is not installed, defaults to "en".

Locale Persistence

When persistLocale is true, every setLocale() call saves to AsyncStorage. On next app launch, the stored locale is restored. Requires @react-native-async-storage/async-storage.

Locale Fallback Chain

Regional subtags are automatically expanded. For locale "ar-EG" with fallbackLocale="en", the chain is:

ar-EG → ar → en

Keys are resolved by trying each locale in order. You can also provide an explicit array:

<RTLProvider fallbackLocale={["fr", "en"]} ...>

Async Translation Loading

Use loadTranslations to lazy-load translations per locale instead of bundling all locales:

<RTLProvider
  defaultLocale="en"
  loadTranslations={async (locale) => {
    const response = await fetch(`https://api.example.com/i18n/${locale}`);
    return response.json();
  }}
  loadingFallback={<Text>Loading...</Text>}
>
  • Each locale is loaded once and cached
  • Fallback chain locales are preloaded in the background
  • Static translations prop takes precedence over async-loaded ones
  • isLoadingTranslations from useRTL() indicates loading state

Hooks

useRTL<TKeys>()

The main hook. Returns everything you need:

const {
  direction,              // "ltr" | "rtl"
  locale,                 // current locale string
  setLocale,              // change locale (uncontrolled mode)
  t,                      // translation function
  formatNumber,           // locale-aware number formatting
  formatDate,             // locale-aware date formatting
  isLocaleReady,          // false while restoring from AsyncStorage
  isLoadingTranslations,  // true while async translations are loading
} = useRTL();

useDirection()

Returns the current direction from the nearest DirectionProvider. Throws if no provider found.

const direction = useDirection(); // "ltr" | "rtl"

useIsRTL()

Shorthand boolean:

const isRTL = useIsRTL(); // true | false

i18n Translations and Pluralization

Nested keys (dot notation)

const translations = {
  en: {
    settings: {
      profile: "Profile",
      notifications: "Notifications",
    },
  },
};

t("settings.profile"); // "Profile"

Interpolation

Use {{var}} placeholders:

// "Hello Ali!"
t("greeting", { name: "Ali" });

Pluralization

Append _zero, _one, _two, _few, _many, _other suffixes. Uses Intl.PluralRules for locale-correct selection:

const translations = {
  en: {
    items_one: "{{count}} item",
    items_other: "{{count}} items",
  },
};

t("items", { count: 1 }); // "1 item"
t("items", { count: 5 }); // "5 items"

Type-safe keys

Get autocomplete on translation keys:

import type { TranslationKeys } from "expo-rtl";
import { translations } from "./translations";

const { t } = useRTL<TranslationKeys<typeof translations>>();
t("settings.profile");  // autocomplete
t("typo");              // TypeScript error

Missing key warnings

In __DEV__, a console.warn is emitted when a translation key is not found.

Locale-Aware Number and Date Formatting

const { formatNumber, formatDate } = useRTL();

formatNumber(1234567.89);                           // "1,234,567.89" (en) / "١٬٢٣٤٬٥٦٧٫٨٩" (ar)
formatNumber(42.5, { style: "currency", currency: "USD" }); // "$42.50" / "٤٢٫٥٠ US$"
formatDate(new Date());                              // "3/21/2026" (en) / "٢١‏/٣‏/٢٠٢٦" (ar)
formatDate(new Date(), { weekday: "long", month: "long", day: "numeric" });

Mixing LTR and RTL in the Same Screen

Override direction for a subtree — all children inherit it:

// Force this section to LTR inside an RTL app
<View dir="ltr" style={{ flexDirection: "row" }}>
  <Text>Always left-to-right</Text>
</View>

RTL Support for NativeWind / Tailwind CSS

expo-rtl supports NativeWind v4 out of the box. All className styles are RTL-flipped just like inline styles.

Setup

  1. Install NativeWind v4:
npm install nativewind@^4 react-native-css-interop tailwindcss@~3
  1. Configure babel.config.js:
module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ["babel-preset-expo", { jsxImportSource: "nativewind" }],
      "nativewind/babel",
    ],
  };
};
  1. Configure tailwind.config.js:
module.exports = {
  content: ["./App.{js,jsx,ts,tsx}", "./screens/**/*.{js,jsx,ts,tsx}"],
  presets: [require("nativewind/preset")],
  theme: { extend: {} },
  plugins: [],
};
  1. Create global.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
  1. Configure metro.config.js:
const { withNativeWind } = require("nativewind/metro");
module.exports = withNativeWind(config, { input: "./global.css" });
  1. Import in your entry file:
import "./global.css";
import "expo-rtl/nativewind";  // registers cssInterop for all expo-rtl components

Usage

import { View, Text, Image, Pressable } from "expo-rtl";

// All className styles flip in RTL automatically
<View className="flex-row gap-4 pl-4">
  <Image flip source={chevron} className="w-4 h-4" />
  <Text className="text-left flex-1">Flips to right in RTL</Text>
</View>

// Logical classes work too (ms-*, me-*, ps-*, pe-*, start-*, end-*)
<View className="ms-4 pe-2">
  <Text className="text-start">Uses logical start/end</Text>
</View>

// noFlip works with className
<View noFlip className="flex-row gap-2">
  <Text>Always LTR</Text>
</View>

Supported NativeWind classes

All Tailwind utility classes that map to directional styles are flipped:

  • Layout: flex-row, flex-row-reverse
  • Spacing: ml-*/mr-*, pl-*/pr-*, ms-*/me-*, ps-*/pe-*
  • Positioning: left-*/right-*, start-*/end-*
  • Text: text-left/text-right, text-start/text-end
  • Borders: border-l-*/border-r-*, border-s-*/border-e-*, rounded-l-*/rounded-r-*, rounded-s-*/rounded-e-*
  • Alignment: items-start/items-end, self-start/self-end (in vertical layouts)

Adding RTL Support to Custom Components

Wrap any React Native component with RTL support:

import { createRTLComponent } from "expo-rtl";
import { MyCustomView } from "./MyCustomView";

export const RTLCustomView = createRTLComponent(MyCustomView, {
  additionalStyleProps: ["contentContainerStyle"], // optional
  isTextLike: true, // optional — adds writingDirection + default textAlign
});

For animated custom components:

import { createRTLAnimatedComponent } from "expo-rtl";
import { Animated } from "react-native";

const MyAnimatedBox = Animated.createAnimatedComponent(MyBox);
export const RTLAnimatedBox = createRTLAnimatedComponent(MyAnimatedBox);

API Reference

Exports from "expo-rtl"

Export Type Description
View, Text, etc. (22) Component RTL-aware drop-in replacements
RTLProvider Component i18n + direction provider
DirectionProvider Component Direction-only provider
useRTL() Hook Direction, locale, t(), formatting, status
useDirection() Hook Current direction
useIsRTL() Hook Boolean RTL check
createRTLComponent() Factory Wrap custom components
createRTLAnimatedComponent() Factory Wrap custom animated components
flipStyle() Utility Flip a style object for RTL
resolveStyle() Utility Flatten + conditionally flip
directionForLocale() Utility Locale string to direction
detectDeviceLocale() Utility Read device locale from expo-localization
buildFallbackChain() Utility Build locale fallback chain from subtags
translate() Utility Standalone translate function
formatNumber() Utility Standalone number formatter
formatDate() Utility Standalone date formatter
Direction Type "ltr" | "rtl"
DirectionProp Type "ltr" | "rtl" | "auto"
Translations Type Translation object shape
TranslationKeys<T> Type Extract keys for type-safe t()
DotPaths<T> Type Dot-notation path extractor

Exports from "expo-rtl/i18n"

Subset: RTLProvider, useRTL, useIsRTL, directionForLocale, detectDeviceLocale, buildFallbackChain, translate, interpolate, formatNumber, formatDate, and all types.

Exports from "expo-rtl/nativewind"

Side-effect import — registers cssInterop for all 22 components.

License

MIT