Direction and bidi
Read when: the app has Arabic (or any right-to-left language), something sits on the wrong side,
a value reads reversed, or you are about to type left, right, ml- or pr-.
Owns: where direction comes from, logical properties, mixed-direction text, input direction,
directional icons, numbers and dates, and the registry's RTL gaps.
TL;DR
- One set of markup runs RTL in Arabic and LTR in English. Nothing is written twice.
- Direction comes from three places:
<html lang dir>synced on every visit,ConfigProvider :dirat the root of every layout, and the registry generated withrtl: true. - Logical properties only:
ms-,pe-,start-,text-start,border-s. - A Latin value inside Arabic copy sits in
dir="ltr"(display) or gets:dir="autoDir(value)"(an input). - A glyph that points takes
rtl:rotate-180orrtl:-scale-x-100. - Switch the language and look at the same screen before calling it done.
Where direction comes from
<html lang dir>, synced on every Inertia visit bysyncDocumentLanguage()(blueprint.md). Inertia swaps pages under one document, so adirthe server wrote on the first load outlives a language switch. Tailwind'srtl:andltr:variants read this attribute.<ConfigProvider :dir :locale>at the root of every layout. Reka reads direction from it, not from<html>. Without it every primitive places its popper and resolves its arrow keys as if the page were LTR: the dropdown opens off-screen and nothing in the CSS explains why. TheToasteralso takes:dir.- The registry generated with
rtl: trueincomponents.json, socomponents/ui/speaks logical properties.
Components read direction from ConfigProvider or CSS, never from document in script.
Layout once, both directions
| Instead of | Write |
|---|---|
ml-2 / mr-2 |
ms-2 / me-2 |
pl-4 / pr-4 |
ps-4 / pe-4 |
left-0 / right-0 |
start-0 / end-0 |
text-left / text-right |
text-start / text-end |
border-l / border-r |
border-s / border-e |
rounded-l-lg / rounded-r-lg |
rounded-s-lg / rounded-e-lg |
space-x-2 on a flex row |
gap-2 |
- Flex and grid order follow
diron their own; don't reverse them. - Centring with
left-1/2 -translate-x-1/2is symmetric and fine. - What logical properties cannot express (a transform, a shadow offset, a background position) takes
rtl:/ltr:variants. Never branch on direction in script for something CSS can state.
Mixed-direction text
A run in a language other than its surroundings must be isolated, or neutral characters (dots, slashes, digits, brackets, trailing punctuation) jump ends and the line renders reversed.
- A Latin value inside an Arabic page (phone numbers, ids, codes, URLs, emails, versions,
permission keys):
dir="ltr"on the element that shows it, or<bdi dir="ltr">. - Inside a translated sentence, wrap the value in the Unicode isolates U+2066 (LRI) and U+2069 (PDI). Add them where the rendered page shows a flip; they are invisible in the source.
- Text a user typed, or a message in either language:
dir="auto"on its element. - Code and commands:
<pre dir="ltr" class="text-start">.
<a :href="customer.website" dir="ltr">{{ customer.website }}</a>
<span class="font-mono text-xs" dir="ltr">{{ permission.key }}</span>
Inputs
- A plain input follows the page direction. Typing Latin into an RTL input works.
dir="ltr"is for display, never for a plain input. It drags the caret to the far edge and puts an Arabic placeholder on the wrong side of its own box.- An input that holds Latin (a code, a URL, a JSON body) takes
:dir="autoDir(form.code)"fromlib/direction.ts.dir="auto"alone reads the value, and an empty box has none, so the browser falls back to LTR;autoDirreturnsundefinedwhile the box is empty (the page decides) and'auto'once there is a value. - An affix with meaning makes the whole group LTR, affix included: a URL prefix, a unit after a number, a dial code before a phone number.
- A leading icon is not an affix. An
InputGroupAddonholding only an icon carries no order, so that group stays in the page direction. Email, password, price: all plain.
Icons that point
- Flip: chevrons, arrows, back and forward, undo and redo, reply, send, external link.
- Never flip: clocks, phones, checkmarks, play buttons, logos, anything with a digit or a letter.
- Rotate (
rtl:rotate-180) a glyph symmetrical about its centre (chevrons, plain arrows). Mirror (rtl:-scale-x-100) one that is not (an arrow entering a door). - Use the left chevron for "previous" and the right one for "next", and let the class flip both. Don't swap icons by direction.
- A directional character typed into copy (
→) can do neither, so it is an icon, never a literal. - A hover nudge with
translate-x-*needs itsrtl:-translate-x-*twin.
<ChevronRightIcon class="rtl:rotate-180" />
<LogInIcon class="rtl:-scale-x-100" />
Numbers, dates and charts
- Latin digits in both languages: numbers with
en-US, dates withar-u-ca-gregory-nu-latnfor Arabic. Format in one helper inlib/, never withtoLocaleStringin a component. - Columns of numbers take
tabular-nums; money and numbers sit indir="ltr"so a minus sign and a currency symbol keep their place. - A date is shown inside
dir="ltr"(DateTimeValuedoes this). - Chart axes stay left to right inside an Arabic page; that is the normal reading of a time series.
Registry gaps
rtl: true rewrites the properties that have logical twins. Anything without one (a transform, a
shadow offset, a background position) is left alone, so a component looks right in English and broken
in Arabic. The fixes are unlayered rules at the foot of assets/app.css, unlayered because Tailwind's
utilities are layered and would otherwise win.
Found in the shadcn-vue registry (checked 2026-11-03; re-check after upgrading):
SheetContentstays physical. It positions onleft-0/right-0whileSidebartreatssideas start and end, so in Arabic the mobile sidebar sits on the wrong edge and slides in from the wrong one. The override flips the edge and the enter/exit translate. It keys on twodata-slotvalues, because the sidebar's mobile branch renames the sheet's slot to"sidebar".AlertDialogHeaderaligns withsm:text-left. Restated astext-align: startinside the same breakpoint, leaving the small-screen centring alone.Switch's thumb travels ontranslate. The override mirrors the checked travel only; unchecked is0in both directions.
Still physical in the registry; add an override the moment you use one:
| Component | What is physical |
|---|---|
ToggleGroupItem |
first:rounded-l-* / last:rounded-r-*, border-l-0, inline paddings |
SidebarRail |
group-data-[side=right]:left-0, group-data-[side=left]:-right-4 |
TabsTrigger |
group-data-vertical/tabs:after:-right-1 (vertical tabs only) |
Already correct: pagination arrows (with rtl:rotate-180), the breadcrumb separator, the sidebar's
collapsible chevron and the toaster.
When adding a registry component that animates or offsets, toggle it once in Arabic before trusting it.
Checking it
Switch the language and look at the same screen. What breaks first: poppers (wrong side), drawers (wrong edge), icons that imply direction, anything with a physical class, and anything that animates or offsets.