English
enforce-logical
oxlint rule that rewrites physical Tailwind CSS utilities into logical, RTL-friendly ones, like ml-4 → ms-4 and pr-2 → pe-2, with an autofix.
At a glance
| Autofix | Editor suggestions | Design system | Options |
|---|---|---|---|
| Yes | Yes | Optional — used when entryPoint is set | allowlist, direction, sizing, entryPoint |
What this rule does
Rewrites physical-direction Tailwind utilities into their logical, writing-mode aware equivalents: ml-4 → ms-4, pr-2 → pe-2, left-0 → inset-s-0, rounded-tl-md → rounded-ss-md, and on the block axis mt-4 → mbs-4, top-0 → inset-bs-0, border-b → border-be. Logical utilities resolve to margin-inline-start / padding-block-end / … in CSS, so they follow the writing direction without any extra code: required if your app ships in Arabic, Hebrew, Farsi, or any other RTL language, and the block axis also follows a vertical writing mode. Autofix on the first offender per location, editor suggestion on subsequent ones.
| Physical | Logical | Axis |
|---|---|---|
ml / mr, pl / pr | ms / me, ps / pe | inline |
scroll-ml / scroll-mr, scroll-pl / scroll-pr | scroll-ms / scroll-me, scroll-ps / scroll-pe | inline |
left / right | inset-s / inset-e (start / end before 4.2) | inline |
border-l / border-r | border-s / border-e | inline |
rounded-l / rounded-r, rounded-tl / -tr / -bl / -br | rounded-s / rounded-e, rounded-ss / -se / -es / -ee | inline |
float-, clear-, text- left / right | float-, clear-, text- start / end | inline |
mt / mb, pt / pb | mbs / mbe, pbs / pbe | block |
scroll-mt / scroll-mb, scroll-pt / scroll-pb | scroll-mbs / scroll-mbe, scroll-pbs / scroll-pbe | block |
top / bottom | inset-bs / inset-be | block |
border-t / border-b | border-bs / border-be | block |
w, min-w, max-w | inline, min-inline, max-inline | inline, with sizing |
h, min-h, max-h | block, min-block, max-block | block, with sizing |
size | inline + block | both, with sizing |
rounded-t-* / rounded-b-* have no block-axis counterpart (Tailwind has no rounded-bs), so they are left alone.
The table also covers the three utilities where the direction is the VALUE rather than part of the property: float-left → float-start (float: inline-start), clear-left → clear-start, and text-left → text-start (text-align: start). Miss those and a codebase can be "fully converted" and still float things to the left in RTL.
The block axis, logical sizing and inset-s-* / inset-e-* shipped in Tailwind 4.2. With a design system the rule checks that your Tailwind has them: on 4.1 it leaves the block axis alone and writes start-0 for left-0. Without settings.tailwindcss.entryPoint there is nothing to check against, so it writes start-0 and only suggests the block-axis rewrites, never autofixes them.
DS-independent in the sense that matters: the mapping table is static and the rule works without an entry point. When one IS configured, the rule also checks that the class it suggests exists — a project with its own @utility ml-huge used to be autofixed to ms-huge, which emits no CSS at all, so the fix applied and the margin silently disappeared.
enforce-logical and enforce-physical are sibling rules — they share a mapping table that one inverts. Enable only one at a time.
Options
direction
'inline' | 'block' | 'both', default 'both'.
Restricts conversion to one axis. 'inline' converts left and right (ml-*, left-*, border-r, float-left, …), the part that matters for RTL. 'block' converts top and bottom (mt-*, top-*, border-b, …), which only differ from their logical forms in a vertical writing mode. A codebase converted for RTL that never sets text vertically can keep 'inline'.
jsonc
{ "tailwindcss/enforce-logical": ["error", { "direction": "inline" }] }sizing
boolean, default false.
Also converts widths and heights: w-* → inline-*, h-* → block-*, their min- / max- forms, and size-* → inline-* block-*. Off by default: sizes are in nearly every class string, and a width equals an inline size in every horizontal writing mode, RTL included. It follows direction — w-* is inline, h-* block, and size-* needs both.
Some sizes have no logical counterpart and are left alone: a width in viewport heights (w-dvh) or a height in viewport widths (h-dvw), max-w-prose, and max-w-screen-*. With a design system a named value is converted only when the two classes declare the same thing: w-* reads --width-* first and inline-* only --container-*, so if your theme defines --width-xs, w-xs and inline-xs differ and w-xs stays as written.
jsonc
{ "tailwindcss/enforce-logical": ["error", { "sizing": true }] }allowlist
string[], default [].
Regex patterns (compiled lazily, invalid ones silently skipped). Classes whose full string matches any pattern bypass the rewrite. Use this for one-off cases where you genuinely want a physical direction — e.g. an icon that should always sit on the visual left regardless of writing direction.
jsonc
{ "tailwindcss/enforce-logical": ["error", { "allowlist": ["^ml-icon$", "^rounded-tl-special$"] }] }entryPoint
string, optional. A CSS entry point for this rule alone, overriding settings.tailwindcss.entryPoint. Only used to confirm that the class being suggested exists and that your Tailwind has the 4.2 utilities; the rule works without it.
Examples
✗ Incorrect
tsx
// Physical margins/padding
<div className="ml-4 pr-2" />
// ~~~~ ~~~~ → ms-4 pe-2
// The direction as a VALUE
<div className="float-left clear-right text-left" />
// ~~~~~~~~~~ ~~~~~~~~~~~ ~~~~~~~~~ → float-start clear-end text-start
// Positioning
<div className="left-0 right-0" />
// ~~~~~~ ~~~~~~~ → inset-s-0 inset-e-0
// Borders and radii
<div className="border-l rounded-tl-md" />
// ~~~~~~~~ ~~~~~~~~~~~~~ → border-s rounded-ss-md
// The block axis
<div className="mt-4 top-0 border-b" />
// ~~~~ ~~~~~ ~~~~~~~~ → mbs-4 inset-bs-0 border-be
// options: { "sizing": true }
// Sizes
<div className="w-full h-screen size-4" />
// ~~~~~~ ~~~~~~~~ ~~~~~~ → inline-full block-screen inline-4 block-4✓ Correct
tsx
// Logical equivalents
<div className="ms-4 pe-2" />
<div className="inset-s-0 inset-e-0" />
<div className="border-s rounded-ss-md" />
<div className="mbs-4 inset-bs-0 border-be" />
// Already logical — variants and important round-trip cleanly
<div className="hover:ms-4 ps-(--gutter) me-4!" />
// options: { "direction": "inline" }
// Only the inline axis: the block axis is left as written
<div className="mt-4 top-0" />Interactions with other rules
enforce-physical: the inverse. They share the mapping table; enabling both at the same time will autofix in a loop. Pick one based on whether your app supports RTL (useenforce-logical) or is LTR-only (useenforce-physical).enforce-canonical: it agrees on the logical insets. On Tailwind 4.2+ this rule writesinset-s-2, the spelling the design system reports as canonical;start-2(what it writes without a design system, or on 4.1) is rewritten toinset-s-2byenforce-canonical. Same CSS either way, andenforce-physicalconverts both spellings back.enforce-shorthand: both rewrite pairs of sides.mt-2 mb-2is a block-axis pair to this rule andmy-2to that one;enforce-shorthandalso folds the logical pairs this rule writes (mbs-2 mbe-2→my-2,inset-s-0 inset-e-0→inset-x-0), so whichever fix lands first the string ends asmy-2, which is already logical (margin-block). Withsizing,w-4 h-4ends asinline-4 block-4either way:enforce-shorthandnever folds that pair intosize-4. The one combination that can end two ways isdirection: 'inline'withsizing:w-4 h-4becomessize-4orinline-4 h-4depending on which fix runs first.
When to disable it
- LTR-only applications where you're sure RTL will never be needed. Use
enforce-physicalinstead if you want consistency in the other direction. - Pixel-perfect layouts where physical direction is part of the design (rare, but happens with icons or decoration). Reach for
allowlistbefore disabling globally.