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

@alialhajhussein/rtl-kit

v0.1.2

Published

Right-to-left done properly for the web and React Native: text direction, bidi isolation for IBANs, phones and numbers, digit normalization, bidi-spoofing detection, style mirroring, and React and React Native helpers.

Downloads

519

Readme

@alialhajhussein/rtl-kit

Right-to-left done properly for the web and React Native. Text direction from Unicode data, bidi isolation for the tokens that actually break, digit normalization for Arabic and Persian keyboards, bidi-spoofing detection, style mirroring, and React and React Native helpers.

Zero dependencies. Tree-shakeable. ESM and CommonJS with types. Runs in browsers, Node, edge runtimes and Hermes.

npm install @alialhajhussein/rtl-kit

| Entry | For | Peer | | --------------------------------- | ------------------------------------------------------------------------------------- | ----------------------- | | @alialhajhussein/rtl-kit | Strings: direction, isolation, digits, bidi controls | none | | @alialhajhussein/rtl-kit/style | Mirroring CSS-in-JS and React Native style objects | none | | @alialhajhussein/rtl-kit/react | DirectionProvider, useDirection, <Isolate>, <Mirrored> | react | | @alialhajhussein/rtl-kit/native | App direction switching, DirectionScope, autoTextStyle, <Isolate>, <Mirrored> | react, react-native |

Strings

import {
  textDirection,
  localeDirection,
  isolate,
  isolateTokens,
  toLatinDigits,
  findBidiControls,
  isBidiBalanced,
  stripBidiControls,
} from "@alialhajhussein/rtl-kit";

textDirection("مرحبا hello"); // "rtl" — first strong character, as dir="auto" decides
textDirection("1234"); // "neutral"
textDirection("1234", "rtl"); // "rtl" — with a fallback

localeDirection("fa-IR"); // "rtl"
localeDirection("pa-PK"); // "rtl" — Punjabi is written in Arabic script in Pakistan
localeDirection("pa-IN"); // "ltr"

isolateTokens("اتصل على +49 176 1234567 الآن");
// the phone number wrapped in LRI…PDI, so its digit groups keep their order

isolate(userName); // FSI…PDI: the plain-text <bdi>

toLatinDigits("٠١٧٦ ١٢٣٤٥٦٧"); // "0176 1234567" — validate input typed on an Arabic keyboard

isBidiBalanced("invoice‮fdp.exe"); // false — an unclosed override, the Trojan Source pattern
stripBidiControls("invoice‮fdp.exe"); // "invoicefdp.exe"

What isolateTokens changes, and what it leaves alone

It isolates only tokens that browsers display out of order inside right-to-left text:

  • Phone numbers that start with + or ( or have spaces between groups, such as +49 176 1234567, 0176 1234567 or (030) 1234-5678. Without isolation the digit groups appear reversed and the + lands on the wrong end. Groups joined only by -, . or / (0176-1234567, 030/12345678) already form one left-to-right run and are left alone.
  • Signed numbers such as -15% or +2,5 %. Without isolation the sign appears on the far side. CLDR's own Arabic and Persian number formats insert a left-to-right mark here for the same reason.

It deliberately leaves alone text that already displays correctly: IBANs, URLs and email addresses (they start with a letter, so their digits resolve left to right by Unicode rule W7), currency amounts (CLDR formats them in right-to-left order), unsigned numbers and dates, and ranges. Text already inside an isolate is skipped, so the function is idempotent. Both behaviours are checked by measuring glyph positions in Chromium, Firefox and WebKit (e2e/).

For names, product titles and other user content, use isolate() or <Isolate>.

Direction data

textDirection uses the bidi classes of every code point in Unicode 17.0. localeDirection uses Unicode script directions and CLDR 48 likely subtags and language aliases, so ckb, ur, sd, pa-PK, uz-AF and Dari's prs are right to left, and ku or pa-Arab resolve by their script. The data is bundled rather than read from Intl.Locale#getTextInfo, which Hermes and older browsers lack, so every runtime gives the same answer. UNICODE_VERSION and CLDR_VERSION are exported.

Style mirroring

import { mirrorStyle, directionalStyle } from "@alialhajhussein/rtl-kit/style";

mirrorStyle({ marginLeft: 8, textAlign: "left", borderRadius: "4px 0 0 4px" });
// { marginRight: 8, textAlign: "right", borderRadius: "0 4px 4px 0" }

mirrorStyle({ transform: [{ translateX: 10 }], shadowOffset: { width: 2, height: 1 } });
// React Native: { transform: [{ translateX: -10 }], shadowOffset: { width: -2, height: 1 } }

directionalStyle(style, direction); // mirrored only when direction is "rtl"

It swaps left and right properties and flips values that encode a side: text-align, float, four-value margins and paddings, corner radii (including the three-value form), shadow offsets, translate, rotate and skew transforms, background-position (and left/right keywords in the background shorthand) and cursors. It handles camelCase and kebab-case keys, nested selectors and media queries, and React Native style arrays, transform arrays and shadow offsets. Its return type has the renamed keys, so TypeScript sees marginRight after mirroring marginLeft. Logical properties such as marginInlineStart are already direction-aware and stay as they are, and url() values are never rewritten. Add /* @noflip */ to a value to keep it.

Its output matches rtl-css-js on that library's cases, except where rtl-css-js is wrong: it leaves three-value radii unmirrored, rewrites url(left.png) to url(right.png), and ignores React Native transforms and shadow offsets.

On the web, prefer logical CSS properties and dir where you can; mirrorStyle is for style objects you don't control or can't rewrite. In an iOS or Android app running right to left, React Native already swaps left and right margins, paddings, positions and text alignment, so mirroring those styles again flips them back.

React

"use client";
import { DirectionProvider, useDirection, Isolate, Mirrored } from "@alialhajhussein/rtl-kit/react";

<DirectionProvider locale={locale} syncDocument>
  <App />
</DirectionProvider>;

const direction = useDirection();            // "ltr" | "rtl"
<p>{t("greeting")} <Isolate>{user.name}</Isolate></p>  // renders <bdi>
<Mirrored><ArrowRightIcon /></Mirrored>      // flipped in rtl

DirectionProvider renders no element. It takes direction or locale, or inherits from the nearest provider. With syncDocument it sets dir and lang on <html> and restores them on unmount. Mirror icons that point along the reading direction (arrows, chevrons, send, reply, undo and redo). Leave checkmarks, media controls, clocks and logos as they are.

React Native

import * as Updates from "expo-updates";
import {
  getAppDirection,
  setAppDirection,
  DirectionScope,
  autoTextStyle,
  Isolate,
  Mirrored,
} from "@alialhajhussein/rtl-kit/native";

await setAppDirection("rtl", { reload: Updates.reloadAsync });
// { changed: true, restartRequired: false }

<Text style={autoTextStyle(message)}>{message}</Text>  // aligned by the message's own direction

<DirectionScope direction="rtl">…</DirectionScope>   // one RTL subtree, no restart

<Text>اتصل على <Isolate dir="ltr">+49 176 1234567</Isolate></Text>
  • setAppDirection persists the direction through I18nManager.allowRTL and forceRTL, regardless of the device locale. Native layout direction is read once at startup, so pass a reload function (Updates.reloadAsync, or DevSettings.reload in development) or let it apply on the next launch. It reports { changed, restartRequired }. Expo Go ignores forced directions, so test in a development build.
  • autoTextStyle(text) is the dir="auto" that React Native lacks: user content aligns to the side its own direction starts on. React Native swaps left and right alignment in right-to-left layouts on both iOS and Android, and this accounts for it; on react-native-web, which does not swap them, it returns the physical side. For alignment that should follow the layout rather than the content, use textAlign: "start" or "end".
  • DirectionScope lays out a subtree right to left (or left to right) through the direction style, and useLayoutDirection() inside it reports that direction. On the web, setAppDirection updates the document without a reload and re-renders useLayoutDirection.

On devices

The example app on the iOS 27 simulator and an Android emulator, in a left-to-right app and after setAppDirection("rtl") and a restart, with @alialhajhussein/modern-arabic-font loaded. The raw phone number shows the bug; the isolated lines show the fix.

| iOS, LTR | iOS, RTL | Android, LTR | Android, RTL | | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | | | | |

Size

Measured with esbuild (minified, gzip) by pnpm size, which fails CI above the budget:

| Import | gzip | | ---------------------------- | ------ | | textDirection | 1.5 kB | | isolateTokens | 0.8 kB | | localeDirection | 1.1 kB | | toLatinDigits | 0.3 kB | | everything in the root entry | 4.2 kB | | /style | 1.6 kB | | /react | 1.6 kB | | /native | 2.6 kB |

Most of the root entry's size is the bundled Unicode and CLDR data.

Development

pnpm install
pnpm test          # unit tests, including every Unicode code point against DerivedBidiClass.txt
pnpm build
pnpm test:e2e      # glyph-position checks in Chromium, Firefox and WebKit (needs pnpm build)
pnpm size
pnpm generate:data # regenerate src/data from Unicode and CLDR

example/ is an Expo app that exercises the React Native entry and runs the core under Hermes.

Licence

MIT © 2026 Ali Al Haj Hussein. The generated tables in src/data are derived from the Unicode Character Database and CLDR, © Unicode, Inc., under the Unicode License v3.