Package Exports
- expo-rtl
- expo-rtl/i18n
- expo-rtl/nativewind
Readme
expo-rtl
Per-component RTL support for React Native — no app restart required.
Drop-in replacements for React Native components that automatically flip styles, layout, and text direction based on a React context. Includes a built-in i18n system with translations, pluralization, locale-aware formatting, auto-detection, persistence, and async loading.
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.
Style Flipping
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 | falseTranslations
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.
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" });Nested Direction Overrides
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>NativeWind Integration
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)
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