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

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.

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

  • Per-component direction -- flip individual subtrees with the dir prop, 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 flip prop
  • 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-kit

Peer Dependencies

react >= 18
react-native >= 0.72

Optional 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:

  1. 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
  2. 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: marginLeft becomes marginRight
  • Logical properties swap: marginStart becomes marginEnd (because React Native resolves logical properties via I18nManager, which is always LTR in rtl-kit)
  • Value properties flip: flexDirection: "row" becomes "row-reverse"
  • Transforms negate: translateX: 100 becomes translateX: -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(); // boolean

Factory

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 needed

RTLProvider

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 error

Locale 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 -> en

Keys 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

  1. TextInput writingDirection -- Changing writingDirection at runtime on a native TextInput causes text to disappear until re-focused. rtl-kit skips writingDirection injection for TextInput and handles alignment via textAlign flipping only.

  2. LayoutAnimation -- LayoutAnimation runs 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.

  3. Horizontal ScrollView position -- Horizontal ScrollViews do not natively start from the right in RTL. rtl-kit adds flexDirection: "row-reverse" to contentContainerStyle as a workaround, but this does not affect the scroll position itself.

  4. Web support -- React Native Web does not support CSS logical properties. rtl-kit maps marginStart to margin-left based on direction, but this is best-effort and may not cover all edge cases.

  5. 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 test

Testing

npm test              # Run all tests
npm test -- --watch   # Watch mode

License

MIT