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 |
|---|---|---|
![]() |
![]() |
![]() |
| 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 components —
View,Text,TextInput,ScrollView,FlatList,Image, and 16 more - Per-component direction — flip individual subtrees with the
dirprop, no global restart - Style flipping — margins, padding, borders, border-radius, absolute positioning, flexDirection, textAlign, alignItems/alignSelf, transforms, logical properties (start/end)
noFlipopt-out — keep specific components LTR even inside an RTL subtree- Image mirroring —
flipprop 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 → enauto-expanded from subtags - Async translation loading — lazy-load translations per locale via
loadTranslations - Locale-aware formatting —
formatNumber()andformatDate()powered byIntl - Type-safe translations — autocomplete on
t()keys viaTranslationKeys<typeof translations> - NativeWind support — optional
classNameinterop with full RTL flipping - Animated components — factory for RTL-aware animated components with auto-negated translateX/scaleX
Installation
npm install expo-rtlPeer 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 v4Quick 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 (left ↔ right) 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 | marginLeft ↔ marginRight, paddingLeft ↔ paddingRight, borderLeftWidth ↔ borderRightWidth, borderLeftColor ↔ borderRightColor, left ↔ right |
| Logical properties | marginStart ↔ marginEnd, paddingStart ↔ paddingEnd, borderStartWidth ↔ borderEndWidth, borderStartColor ↔ borderEndColor, start ↔ end |
| Border radius (physical) | borderTopLeftRadius ↔ borderTopRightRadius, borderBottomLeftRadius ↔ borderBottomRightRadius |
| Border radius (logical) | borderTopStartRadius ↔ borderTopEndRadius, borderBottomStartRadius ↔ borderBottomEndRadius |
| 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/marginEndetc. viaI18nManager, 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 likems-*,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 → enKeys 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
translationsprop takes precedence over async-loaded ones isLoadingTranslationsfromuseRTL()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 | falsei18n 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 errorMissing 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
- Install NativeWind v4:
npm install nativewind@^4 react-native-css-interop tailwindcss@~3- Configure
babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};- Configure
tailwind.config.js:
module.exports = {
content: ["./App.{js,jsx,ts,tsx}", "./screens/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: { extend: {} },
plugins: [],
};- Create
global.css:
@tailwind base;
@tailwind components;
@tailwind utilities;- Configure
metro.config.js:
const { withNativeWind } = require("nativewind/metro");
module.exports = withNativeWind(config, { input: "./global.css" });- Import in your entry file:
import "./global.css";
import "expo-rtl/nativewind"; // registers cssInterop for all expo-rtl componentsUsage
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


