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
Live. The sidebar, the tables and the code blocks all move
<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.

Physical properties and their logical equivalents
Instead ofDeck writesWhich is
padding-leftpadding-inline-startThe edge text starts at
margin-rightmargin-inline-endThe edge text ends at
text-align: lefttext-align: startRagged edge follows the language
widthinline-sizeAlong the text axis
top / bottominset-block-start / endAlong 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:

RTL mirroring utilities
Class Declares Resolves to Source
.mirror-rtl [dir="rtl"] .mirror-rtl
scale: -1 1
— 19-logical.css:133
.no-flip [dir="rtl"] .no-flip
scale: 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.

Direction and bidi utilities
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-left and text-align: right. Deck has none; your layer is where they will be.