npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-rtl

v1.0.1

Published

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

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