rtl-kit
v0.1.0
Published
RTL (right-to-left) layout support for React Native — per-component, no restart required. Drop-in components, style flipping, Reanimated support, and NativeWind integration.
Maintainers
Readme
rtl-kit
Per-component RTL (right-to-left) layout support for React Native. No restart required.
rtl-kit provides drop-in replacements for React Native components that automatically flip styles, margins, padding, borders, transforms, and text direction when the layout direction is RTL. Mix LTR and RTL in the same screen using nested direction providers.
Table of Contents
- Features
- Installation
- Quick Start
- Why rtl-kit
- Concepts
- API Reference
- Style Flipping Rules
- Known Limitations
- Contributing
- License
Features
- Per-component direction -- flip individual subtrees with the
dirprop, no global restart - 22 drop-in components -- View, Text, TextInput, ScrollView, FlatList, Image, and 16 more
- Automatic style flipping -- margins, padding, borders, border-radius, absolute positioning, flexDirection, textAlign, alignItems, alignSelf, transforms, logical properties
- NoFlip opt-out -- keep specific components LTR even inside an RTL subtree
- Image mirroring -- flip directional icons (arrows, chevrons) with the
flipprop - Built-in i18n -- translations with interpolation, pluralization, locale-aware formatting
- Locale persistence -- save and restore locale via AsyncStorage
- Async translation loading -- lazy-load translations per locale
- NativeWind support -- className styles are flipped automatically
- Uniwind support -- pre-wrapped components with full RTL flipping
- Zero runtime dependencies -- pure TypeScript, uses React Native and browser APIs
Installation
npm install rtl-kitPeer Dependencies
react >= 18
react-native >= 0.72Optional Dependencies
| Package | Purpose |
|---------|---------|
| react-native-safe-area-context | SafeAreaView component |
| expo-localization | Auto-detect device locale |
| @react-native-async-storage/async-storage | Persist locale choice |
| nativewind + react-native-css-interop | NativeWind className support |
| uniwind + tailwindcss | Uniwind className support |
Expo Compatibility
rtl-kit works in Expo apps with zero extra configuration -- no config plugins, no prebuild, no native setup. It is a pure JS library with no native modules.
npx expo install rtl-kit
# That's it. Works in Expo Go and managed workflow.Subpath imports (rtl-kit/icon, rtl-kit/hooks, etc.) require Expo SDK 53+ (React Native 0.79+) because Metro only added stable exports field support in that version. On Expo SDK 52 and earlier, import everything from the main entry point:
// Expo SDK 53+ (recommended -- enables tree-shaking)
import { View } from "rtl-kit";
import { RTLIcon } from "rtl-kit/icon";
// Expo SDK 52 and earlier (still works, no tree-shaking)
import { View, RTLIcon } from "rtl-kit";Quick Start
1. Wrap your app
import { DirectionProvider } from "rtl-kit";
export default function App() {
return (
<DirectionProvider dir="ltr">
<MyApp />
</DirectionProvider>
);
}2. Use RTL-aware components
import { View, Text, Image } from "rtl-kit";
function Header() {
return (
<View style={{ flexDirection: "row", marginLeft: 16 }}>
<Image
flip
source={require("./arrow-right.png")}
style={{ width: 24, height: 24 }}
/>
<Text style={{ flex: 1 }}>Welcome</Text>
</View>
);
}When direction is RTL, the row reverses, the margin moves to the right side, and the arrow image mirrors horizontally.
3. Switch direction per component
<View dir="rtl" style={{ flexDirection: "row", paddingLeft: 16 }}>
<Text>This section is RTL</Text>
</View>
<View style={{ flexDirection: "row", paddingLeft: 16 }}>
<Text>This section inherits parent direction</Text>
</View>Why rtl-kit
React Native's built-in I18nManager.forceRTL() has two significant limitations:
- Requires an app restart -- the native layout engine reads the RTL flag at launch, so changes do not take effect until the user restarts the app
- App-wide only -- you cannot mix LTR and RTL sections in the same screen
rtl-kit solves both problems by implementing RTL at the React layer. Styles are flipped during render, direction flows through React context, and different subtrees can have different directions simultaneously.
| | I18nManager.forceRTL() | rtl-kit | |---|---|---| | Granularity | App-wide only | Per-component | | 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 (logical props only) | All physical, logical, and transforms |
Concepts
Direction Context
Direction flows through React context. Every rtl-kit component reads the current direction from the nearest DirectionProvider. You can nest providers to override direction for a subtree:
<DirectionProvider dir="ltr">
<View>
<Text>LTR</Text>
<DirectionProvider dir="rtl">
<View>
<Text>RTL</Text>
</View>
</DirectionProvider>
</View>
</DirectionProvider>Style Flipping
When a component's direction is RTL, its style prop is automatically transformed:
- Physical properties swap:
marginLeftbecomesmarginRight - Logical properties swap:
marginStartbecomesmarginEnd(because React Native resolves logical properties viaI18nManager, which is always LTR in rtl-kit) - Value properties flip:
flexDirection: "row"becomes"row-reverse" - Transforms negate:
translateX: 100becomestranslateX: -100
Results are cached in a WeakMap keyed by the style object reference. Styles created with StyleSheet.create() are cached permanently. Inline styles are re-flipped each render, which is acceptable for most cases.
noFlip
Any component can opt out of style flipping with the noFlip prop. Its children still inherit the current direction and flip their own styles:
<View dir="rtl" style={{ flexDirection: "row" }}>
<View noFlip style={{ flexDirection: "row" }}>
<Text>This row is always LTR</Text>
</View>
<Text>This text is RTL-aligned</Text>
</View>API Reference
Components
All 22 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 rtl-kit/nativewind import) |
Available components:
View, Text, TextInput, ScrollView, FlatList, SectionList, VirtualizedList, Pressable, TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, TouchableNativeFeedback, Image, ImageBackground, SafeAreaView, KeyboardAvoidingView, Modal, Switch, ActivityIndicator, Button, DrawerLayoutAndroid, RefreshControl
Image
Image has an additional flip prop that mirrors the image horizontally in RTL. This is useful for directional icons like arrows and chevrons:
<Image
flip
source={require("./chevron-right.png")}
style={{ width: 24, height: 24 }}
/>ScrollView / FlatList
Horizontal scroll components automatically start from the correct side in RTL. For horizontal ScrollViews, contentContainerStyle receives flexDirection: "row-reverse". For horizontal FlatLists, the inverted prop is toggled.
Text and TextInput
Text components receive writingDirection and default textAlign based on direction. TextInput skips writingDirection to avoid a React Native rendering bug where text disappears until re-focused.
DirectionProvider
Provides text and layout direction to all rtl-kit components in its subtree.
import { DirectionProvider } from "rtl-kit";
// Explicit direction
<DirectionProvider dir="rtl">
<App />
</DirectionProvider>
// Auto-resolve from locale
<DirectionProvider lang="ar">
<App />
</DirectionProvider>
// Default direction (when no provider exists)
<DirectionProvider defaultDirection="ltr">
<App />
</DirectionProvider>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| dir | "ltr" \| "rtl" \| "auto" | -- | Explicit direction. Takes precedence over lang. |
| lang | string | -- | BCP 47 locale tag. Auto-resolves direction. |
| defaultDirection | "ltr" \| "rtl" | "ltr" | Fallback when neither dir nor lang is provided. |
| children | ReactNode | -- | Child components. |
Hooks
useDirection
Returns the current direction from the nearest DirectionProvider. Throws if no provider exists.
import { useDirection } from "rtl-kit";
const direction = useDirection(); // "ltr" | "rtl"useIsRTL
Returns true if the current direction is RTL.
import { useIsRTL } from "rtl-kit";
const isRTL = useIsRTL(); // booleanFactory
createRTLComponent
Wraps any React Native component with RTL support. Most rtl-kit components are implemented with this factory:
import { createRTLComponent } from "rtl-kit";
import { MyCustomView } from "./MyCustomView";
const RTLCustomView = createRTLComponent(MyCustomView);Options
| Option | Type | Description |
|--------|------|-------------|
| additionalStyleProps | string[] | Additional style props to flip (e.g. ["contentContainerStyle"]) |
| isTextLike | boolean | Inject writingDirection and default textAlign |
| isTextInput | boolean | Inject default textAlign only (skips writingDirection) |
const RTLVirtualizedList = createRTLComponent(VirtualizedList, {
additionalStyleProps: ["contentContainerStyle"],
});
const RTLText = createRTLComponent(Text, { isTextLike: true });i18n
The i18n module is available from rtl-kit/i18n. It provides a full internationalization system with translations, pluralization, and locale-aware formatting.
npm install rtl-kit
# i18n is included -- no separate package neededRTLProvider
Combines DirectionProvider with i18n context. When the locale changes, direction is automatically resolved.
import { RTLProvider } from "rtl-kit/i18n";
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: "الملف الشخصي",
},
},
};
// Uncontrolled mode (recommended)
<RTLProvider defaultLocale="en" fallbackLocale="en" translations={translations}>
<App />
</RTLProvider>
// Auto-detect device locale
<RTLProvider fallbackLocale="en" translations={translations}>
<App />
</RTLProvider>
// Persist locale to AsyncStorage
<RTLProvider defaultLocale="en" persistLocale translations={translations}>
<App />
</RTLProvider>
// Controlled mode
<RTLProvider locale={myLocale} translations={translations}>
<App />
</RTLProvider>
// Async translation loading
<RTLProvider
defaultLocale="en"
loadTranslations={(locale) =>
fetch(`/i18n/${locale}.json`).then((r) => r.json())
}
loadingFallback={<LoadingScreen />}
>
<App />
</RTLProvider>RTLProvider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| locale | string | -- | Controlled locale |
| defaultLocale | string | -- | Uncontrolled locale |
| fallbackLocale | string \| string[] | "en" | Fallback when a key is missing |
| translations | Translations | -- | Static translation strings |
| loadTranslations | (locale) => Promise<TranslationMap> | -- | Async loader |
| loadingFallback | ReactNode | -- | Shown while loading |
| persistLocale | boolean | false | Save to AsyncStorage |
| children | ReactNode | -- | Child components |
useRTL
The central hook for the i18n module. Provides direction, locale, translations, and formatting.
import { useRTL } from "rtl-kit/i18n";
function MyComponent() {
const {
direction, // "ltr" | "rtl"
isRTL, // boolean
locale, // "en" | "ar" | ...
setLocale, // (locale: string) => void
t, // translation function
formatNumber, // (value, options?) => string
formatDate, // (date, options?) => string
isLocaleReady, // boolean
isLoadingTranslations, // boolean
} = useRTL();
return (
<View>
<Text>{t("greeting", { name: "Ali" })}</Text>
<Text>{formatNumber(1234.56)}</Text>
<Button title="Switch to Arabic" onPress={() => setLocale("ar")} />
</View>
);
}Translation Function
Supports nested keys, interpolation, and pluralization.
// Simple key
t("greeting") // "Hello!"
// With interpolation
t("greeting", { name: "Ali" }) // "Hello Ali!"
// With pluralization
t("items", { count: 1 }) // "1 item"
t("items", { count: 5 }) // "5 items"
// Nested keys
t("settings.profile") // "Profile"Pluralization
Uses Intl.PluralRules for locale-correct selection. Append suffixes to translation keys:
| Suffix | When |
|--------|------|
| _zero | Count is 0 (Arabic, Welsh) |
| _one | Count is 1 |
| _two | Count is 2 (Arabic) |
| _few | Small number (Polish, Russian) |
| _many | Larger number (Polish, Russian) |
| _other | Default fallback |
Interpolation
Use {{variable}} placeholders in translation strings:
const translations = {
en: {
greeting: "Hello {{name}}!",
items: "{{count}} items in {{location}}",
},
};
t("greeting", { name: "Ali" }) // "Hello Ali!"
t("items", { count: 5, location: "cart" }) // "5 items in cart"Type-Safe Keys
Get autocomplete and type checking on translation keys:
import type { TranslationKeys } from "rtl-kit/i18n";
import { translations } from "./translations";
const { t } = useRTL<TranslationKeys<typeof translations>>();
t("settings.profile") // autocomplete
t("typo") // TypeScript errorLocale Detection
When no locale or defaultLocale is provided, rtl-kit 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.
Fallback Chain
Regional subtags are automatically expanded. For locale "ar-EG" with fallbackLocale="en":
ar-EG -> ar -> enKeys are resolved by trying each locale in order until found.
Number and Date Formatting
const { formatNumber, formatDate } = useRTL();
formatNumber(1234567.89) // "1,234,567.89" (en) / "1٬234٬567٫89" (ar)
formatNumber(42.5, { style: "currency", currency: "USD" }) // "$42.50"
formatDate(new Date()) // "3/21/2026" (en) / "21/3/2026" (ar)
formatDate(new Date(), { weekday: "long", month: "long", day: "numeric" })NativeWind
rtl-kit supports NativeWind v4 out of the box. Import the interop module as a side-effect:
import "rtl-kit/nativewind";All className styles are RTL-flipped just like inline styles:
import { View, Text } from "rtl-kit";
<View className="flex-row gap-4 pl-4">
<Text className="text-left flex-1">Flips to right in RTL</Text>
</View>
// Logical classes work too
<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:
- 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)
Uniwind
rtl-kit also ships pre-wrapped components for Uniwind. Import from rtl-kit/uniwind instead of rtl-kit:
import { View, Text, Image, Pressable } from "rtl-kit/uniwind";
<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>All className and *ClassName props (contentContainerClassName, columnWrapperClassName, imageClassName) are flipped for RTL.
Utilities
flipStyle
Flips a flattened style object for RTL. Returns a new object, never mutates the input.
import { flipStyle } from "rtl-kit";
flipStyle({ marginLeft: 16 }) // { marginRight: 16 }
flipStyle({ flexDirection: "row" }) // { flexDirection: "row-reverse" }
flipStyle({ transform: [{ translateX: 100 }] }) // { transform: [{ translateX: -100 }] }resolveStyle
Flattens a style prop and flips it if direction is RTL. No-op for LTR.
import { resolveStyle } from "rtl-kit";
resolveStyle([{ marginLeft: 16 }], "rtl") // { marginRight: 16 }
resolveStyle([{ marginLeft: 16 }], "ltr") // { marginLeft: 16 }directionForLocale
Resolves a BCP 47 locale tag to a layout direction.
import { directionForLocale } from "rtl-kit";
directionForLocale("ar") // "rtl"
directionForLocale("ar-EG") // "rtl"
directionForLocale("en") // "ltr"
directionForLocale("en-US") // "ltr"detectDeviceLocale
Auto-detects the device locale via expo-localization. Falls back to "en" if not installed.
import { detectDeviceLocale } from "rtl-kit";
const locale = detectDeviceLocale(); // "en-US" | "ar" | ...translate
Standalone translation function for use outside of React hooks.
import { translate } from "rtl-kit/i18n";
const translations = {
en: { greeting: "Hello {{name}}!" },
ar: { greeting: "{{name}} !مرحبا" },
};
translate("greeting", ["en"], translations, { name: "Ali" }) // "Hello Ali!"Types
// Direction
type Direction = "ltr" | "rtl";
type DirectionProp = "ltr" | "rtl" | "auto";
// Translations
interface TranslationMap {
[key: string]: string | TranslationMap;
}
type TranslationValue = string | TranslationMap;
type Translations = Record<string, TranslationMap>;
// Type-safe keys
type DotPaths<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? DotPaths<T[K], Prefix extends "" ? K : `${Prefix}.${K}`>
: Prefix extends ""
? K
: `${Prefix}.${K}`
}[keyof T & string]
: never;
type TranslationKeys<T extends Translations> = {
[Locale in keyof T]: DotPaths<T[Locale]>;
}[keyof T];Style Flipping Rules
Property Swaps
| Property A | Property B | Category |
|-----------|-----------|----------|
| marginLeft | marginRight | Physical |
| paddingLeft | paddingRight | Physical |
| borderLeftWidth | borderRightWidth | Physical |
| borderLeftColor | borderRightColor | Physical |
| borderTopLeftRadius | borderTopRightRadius | Physical |
| borderBottomLeftRadius | borderBottomRightRadius | Physical |
| left | right | Physical |
| marginStart | marginEnd | Logical |
| paddingStart | paddingEnd | Logical |
| borderStartWidth | borderEndWidth | Logical |
| borderStartColor | borderEndColor | Logical |
| borderTopStartRadius | borderTopEndRadius | Logical |
| borderBottomStartRadius | borderBottomEndRadius | Logical |
| start | end | Logical |
Value Flips
| Property | RTL Value | LTR Value |
|----------|----------|----------|
| flexDirection | "row-reverse" | "row" |
| flexDirection | "row" | "row-reverse" |
| textAlign | "right" | "left" |
| textAlign | "left" | "right" |
| textAlign | "end" | "start" |
| textAlign | "start" | "end" |
Cross-Axis Flips (Vertical Layouts Only)
| Property | RTL Value | LTR Value |
|----------|----------|----------|
| alignSelf | "flex-end" | "flex-start" |
| alignSelf | "flex-start" | "flex-end" |
| alignItems | "flex-end" | "flex-start" |
| alignItems | "flex-start" | "flex-end" |
Cross-axis alignment only flips in vertical (column) layouts. In row layouts, the cross-axis is vertical, so flipping would incorrectly swap top and bottom.
Transform Negation
| Transform | Effect |
|-----------|--------|
| translateX | Negate numeric values |
| scaleX | Negate numeric values |
Known Limitations
TextInput writingDirection -- Changing
writingDirectionat runtime on a native TextInput causes text to disappear until re-focused. rtl-kit skipswritingDirectioninjection for TextInput and handles alignment viatextAlignflipping only.LayoutAnimation --
LayoutAnimationruns on the native thread and cannot be intercepted by the JS layer. Layout animations will not respect RTL direction changes. Use Reanimated layout animations instead.Horizontal ScrollView position -- Horizontal ScrollViews do not natively start from the right in RTL. rtl-kit adds
flexDirection: "row-reverse"tocontentContainerStyleas a workaround, but this does not affect the scroll position itself.Web support -- React Native Web does not support CSS logical properties. rtl-kit maps
marginStarttomargin-leftbased on direction, but this is best-effort and may not cover all edge cases.Performance with inline styles -- WeakMap caching works best with
StyleSheet.create(), which returns stable object references. Inline style objects created during render will be re-flipped on every render. This is acceptable for most cases but may impact performance in lists with complex inline styles.
Contributing
Contributions are welcome. Please open an issue or pull request.
Development
git clone https://github.com/user/rtl-kit.git
cd rtl-kit
npm install
npm run typecheck
npm testTesting
npm test # Run all tests
npm test -- --watch # Watch modeLicense
MIT
