New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

rtl-lint

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rtl-lint

Find layout that breaks in right-to-left languages: physical CSS properties, directional Tailwind utilities, and missing dir attributes.

latest
Source
npmnpm
Version
0.3.0
Version published
Weekly downloads
12
-33.33%
Maintainers
1
Weekly downloads
 
Created
Source

rtl-lint

npm CI Node License: MIT Dependencies

Find the layout that breaks when your app switches to Arabic, Hebrew, Persian, or Urdu — before a user does.

ml-4 always pushes away from the left edge. In an RTL locale that is the wrong edge, so the icon that sat neatly beside the search field jumps to the far side of it. These bugs never show up in an English screenshot, and there is nothing in your test suite that catches them.

rtl-lint reads your stylesheets and components and reports every place that is pinned to a physical side, with the logical replacement for each one.

  • No dependencies. One package, nothing transitive.
  • No config needed. Point it at a directory.
  • Not an ESLint plugin. A standalone CLI, so it works the same on CSS, HTML, Vue, Svelte, Astro and JSX.

Install

npm install -D rtl-lint

Or run it once without installing:

npx rtl-lint src

Usage

rtl-lint src
src/screens/POS.tsx
  60:51    error   `right-3` is fixed to one side and will not flip in RTL.  tailwind-directional
                   → Use `end-3` (Tailwind 3.3+).
  62:32    error   `pr-10` is fixed to one side and will not flip in RTL.  tailwind-directional
                   → Use `pe-10` (Tailwind 3.3+).

src/components/Sidebar.css
  14:3     error   `margin-left` is fixed to one side and will not flip in RTL.  css-physical-property
                   → Use `margin-inline-start`.

3 errors, 0 warnings in 2 of 50 files
OptionEffect
--base-dir <ltr|rtl>The document's base direction. Decides whether left maps to start or to end. Default ltr
--fixRewrite what can be repaired automatically
--dry-runWith --fix, print the diff instead of writing
--jsonMachine-readable output for CI or an editor integration
--strictExit non-zero on warnings too, not just errors
--ignore <name>Skip a directory by name; repeatable
--no-colorPlain output (also honours NO_COLOR)

Exit code is 1 when errors are found, 0 when clean, 2 on bad usage — so it drops straight into CI:

- run: npx rtl-lint src

What it catches

RuleSeverityCatchesSuggests
css-physical-propertyerrormargin-left, padding-right, border-left-width, border-top-left-radius, left, right, and the scroll-* variants — in CSS, SCSS, Less, and camelCased in CSS-in-JSmargin-inline-start, padding-inline-end, inset-inline-start, …
css-physical-valueerrortext-align: left, float: right, clear: lefttext-align: start, float: inline-end, …
tailwind-directionalerrorml-* mr-* pl-* pr-* left-* right-* text-left text-right float-left rounded-l-* border-r-* scroll-ml-* and friends, including variants (md:hover:-ml-2) and !importantms-*, me-*, ps-*, pe-*, start-*, end-*, text-start, …
missing-direrror<html> with no dir attributeSet dir from the active locale
hardcoded-dirwarningdir="ltr" written into the markupDerive it from the locale
directional-transformwarningrotate(90deg), skewX(…) — chevrons and arrows built this way keep pointing the same way in RTLCheck by eye; mirror under [dir='rtl'] if it conveys direction

Comments are masked before any rule runs, so a property inside /* … */ is never reported — and line numbers still match the original file exactly.

Tailwind's logical utilities (ms-*, text-start, rounded-s-*) need Tailwind CSS 3.3 or newer.

If your app is Arabic-first, set --base-dir rtl

Which logical side a physical one maps to depends on the base direction of the document:

base ltrbase rtl
leftstartend
rightendstart

In an English-first app, text-right means "the end side" and becomes text-end. In an app whose root is <html dir="rtl">, the same class means "the natural reading side" and becomes text-start. Take the default and you mirror a working layout — worse than leaving it alone.

rtl-lint src --base-dir rtl --fix --dry-run

Utilities you already scoped are left alone

ltr:left-3 rtl:right-3 is a correct, deliberate pair: you have already told the browser what to do in each direction. Nothing with an ltr: or rtl: variant is reported or rewritten.

Fixing

rtl-lint src --fix --dry-run   # show the diff
rtl-lint src --fix             # write it

Always look at the diff first. Physical is sometimes deliberate — a logo pinned to the left of a bilingual header is meant to stay there — and no linter can tell that apart from a mistake.

Only mechanical findings are rewritten. A missing dir, a hardcoded dir="ltr", a rotated chevron: those are reported and left alone, because guessing at intent is how an autofixer loses people's trust.

Fixes replace one token at a time. Whitespace, quoting and line count come out exactly as the author wrote them, and running --fix twice changes nothing the second time.

Keeping a physical value on purpose

Put one of these in a comment, in whatever syntax the file already uses:

/* rtl-lint-disable-next-line */
.logo { margin-left: 16px; }
DirectiveScope
rtl-lint-disable-filethe whole file
rtl-lint-disable-linethe line the comment is on
rtl-lint-disable-next-linethe line after

What it does not do

Being honest about the edges, because a linter you cannot trust is worse than none:

  • It reads text, not an AST. Class names are found inside class/className attributes, including JSX expressions and clsx(...) calls. Classes assembled at runtime from variables are invisible to it.
  • Arbitrary variants such as [&:hover]:ml-4 are skipped rather than half-parsed.
  • Physical is sometimes correct. Every finding is a question, not a verdict. Review before you change.

API

import { lintFiles, fixFiles, lintSource } from "rtl-lint";

const { findings, errors, warnings } = await lintFiles("src");

// Repair in place; pass { dryRun: true } to get diffs back instead.
const { fixed, remaining } = await fixFiles("src", { baseDir: "rtl" });

// Or lint a string directly — the filename only picks which rules apply.
const issues = lintSource('<div class="ml-4">', "Button.tsx");

Each finding is { rule, severity, line, column, message, suggestion }, plus file when it came from lintFiles, and fix: { start, end, text } when it can be repaired automatically.

Requirements

Node.js 18.18 or newer.

License

MIT © Khalel Hawary

Keywords

rtl

FAQs

Package last updated on 09 Aug 2026

Related posts