Support right-to-left languages
You need an Arabic, Hebrew, Persian or Urdu version of your interface and you are bracing for a second stylesheet. There is not one. Deck is written entirely in logical properties, so the whole layout mirrors from one attribute — and the work that is left is deciding what must not mirror.
The short answer
<html lang="ar" dir="rtl">
That is the layout done. Padding, margins, borders, radii, flex and grid alignment,
table columns, the drawer's edge, the toast region's corner, the sidebar's side — all
of it flips, because none of it was written in left and
right to begin with.
From JavaScript, Deck.dir() does the same and persists the choice:
Deck.dir(); // 'ltr'
Deck.dir('rtl'); // flip, and remember it
<div class="cluster">
<button class="btn btn-sm" onclick="Deck.dir('rtl')">Flip this page to RTL</button>
<button class="btn btn-sm btn-ghost" onclick="Deck.dir('ltr')">Back to LTR</button>
</div>
<div class="cluster">
<button class="btn btn-sm" onclick="Deck.dir('rtl')">Flip this page to RTL</button>
<button class="btn btn-sm btn-ghost" onclick="Deck.dir('ltr')">Back to LTR</button>
</div>
dir sets direction, not language. It does not translate anything, and an
English page in RTL is a test rather than a localisation. Set lang too:
it drives hyphenation, quotation marks, the font stack's script coverage and how a
screen reader pronounces the page.
Why it costs nothing
Every spacing and alignment property in Deck names the writing axis rather than the
screen. inline is the direction text runs; block is the
direction lines stack. In English, inline-start is the left edge. In Arabic it is the
right edge, and the same declaration means the same thing in both.
| Instead of | Deck writes | Which is |
|---|---|---|
padding-left | padding-inline-start | The edge text starts at |
margin-right | margin-inline-end | The edge text ends at |
text-align: left | text-align: start | Ragged edge follows the language |
width | inline-size | Along the text axis |
top / bottom | inset-block-start / end | Along the line-stacking axis |
The utilities are named the same way, which is why there is no .ml-4 in
Deck — only .mis-4. That is not pedantry: naming the left edge is how you
build the bug in, and a class called ml that sets
margin-inline-start would be worse, because it would lie.
The spacing reference has the full set.
Icons that point
An arrow, a chevron, a reply glyph and a back button all mean "the direction of
reading", so they have to mirror. A checkmark, a clock, a wrench and your logo do not.
Deck mirrors ten of its own sprite icons automatically by matching the
href they use:
[dir="rtl"] :is(
.icon:has(use[href$="#chevron-left"]),
.icon:has(use[href$="#arrow-right"]),
.icon:has(use[href$="#send"]),
…
) { scale: -1 1; }
For your own icons, two classes:
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.mirror-rtl |
[dir="rtl"] .mirror-rtlscale: -1 1
|
— | 19-logical.css:133 |
.no-flip |
[dir="rtl"] .no-flipscale: 1 1
|
— | 19-logical.css:150 |
.mirror-rtl flips an element when the page is RTL.
.no-flip pins one that would otherwise be flipped by a rule above it — a
logo inside a mirrored header, a play button, a chart axis.
What must never flip
This is where RTL bugs actually live. Some values have one true direction regardless of
the page around them, and if you do not isolate them the bidirectional algorithm
reorders their punctuation at the boundary. The classic symptom is a file path whose
trailing slash has migrated to the front, or a version number that reads
1.2.3 in English and 3.2.1 in Arabic.
| Class | Declares | Resolves to | Source |
|---|---|---|---|
.dir-ltr |
direction: ltr; unicode-bidi: isolate
|
— | 19-logical.css:114 |
.dir-rtl |
direction: rtl; unicode-bidi: isolate
|
— | 19-logical.css:115 |
.bidi-isolate |
unicode-bidi: isolate
|
— | 19-logical.css:116 |
.bidi-plaintext |
unicode-bidi: plaintext
|
— | 19-logical.css:117 |
.code-ltr |
direction: ltr; unicode-bidi: isolate; text-align: start
|
— | 19-logical.css:123 |
.code-ltr- A path, a URL, a command, a code span. Always left to right, always isolated, and aligned to the reading edge so it still sits correctly in the paragraph.
.bidi-isolate- A run whose direction you know but which differs from the page — an English product name inside an Arabic sentence.
.bidi-plaintext- A run whose direction you do not know: a chat message, a comment, a user-supplied display name. The direction is taken from the first strong character in the string, so an Arabic message and an English message in the same list each read correctly. This is the one to reach for on anything a person typed.
Deck already isolates code, kbd, samp,
.mono and .nums for you, so identifiers and figures in the
default elements are safe without you doing anything. The gap is user-generated text
and anything you build out of a plain <span>.
Motion flips too
The entrance animations move toward the reader, so their keyframes mirror with the
page. .enter-start travels from the right on an RTL page and from the left
on an LTR one, because "start" is a direction and not a side. You do not have to do
anything; it is worth knowing because an animation that slides the wrong way is a
subtle wrongness that is hard to name when you see it.
Checking it
-
Flip the whole page with the button above, or set
dir="rtl"in dev tools on the<html>element. Look at the sidebar, the table headers, the form labels, the toast corner and the drawer edge. - Look for a scrollbar. Something written in physical properties usually shows up as horizontal overflow on the flipped side.
- Find every number, path, code span and identifier and check nothing has been reordered. Punctuation at the ends of a run is the tell.
- Test with real Arabic or Hebrew text, not English in RTL. Line height, glyph height and the absence of letter casing all change the vertical rhythm, and Latin text flipped left to right hides all three.
-
Grep your own CSS for
left,right,margin-leftandtext-align: right. Deck has none; your layer is where they will be.