ESC

Type to search the knowledge base.

Writing Modes and Direction

direction, writing-mode, and text-orientation — vertical text, RTL layout, and why logical properties beat left/right hardcoding.

advanced3 min read
  • css
  • writing-mode
  • direction
  • i18n

CSS layout has a writing mode (horizontal or vertical) and a direction (LTR/RTL). Together they define the block and inline axes. If your UI hard-codes margin-left and width for everything, vertical text and Arabic/Hebrew layouts become a second product.

Docs: MDN writing modes, writing-mode, direction.

direction and the dir attribute

<html lang="ar" dir="rtl">
. treemirror {
  direction: ltr; /* isolate a code block inside RTL page */
  unicode-bidi: isolate;
}

Prefer the HTML dir attribute for document/section direction so the whole stack (CSS, form controls, browser UA) agrees. CSS direction is useful for islands (code, phone numbers).

writing-mode

.spine-label {
  writing-mode: vertical-rl;
  text-orientation: mixed;
}

.timeline-year {
  writing-mode: vertical-lr;
}
Value Lines progress Common use
horizontal-tb Top → bottom (default) Most UI
vertical-rl Right → left vertical CJK spines, some vertical UI
vertical-lr Left → right vertical Some vertical timelines

When writing mode is vertical, block axis is horizontal — width/height mental models flip; logical properties (inline-size, block-size) stay stable.

text-orientation

.vertical-nav {
  writing-mode: vertical-rl;
  text-orientation: upright; /* Latin letters upright stacked */
}

mixed (default) vs upright vs sideways changes how non-CJK characters sit in vertical lines.

Flex/grid interaction

.stack {
  display: flex;
  flex-direction: column; /* follows writing mode’s block direction? */
}

Flex row/column map to the current writing mode’s axes. Test vertical-rl toolbars explicitly — “row” may not match your physical intuition.

.toolbar {
  display: flex;
  gap: 0.5rem;
  flex-direction: row; /* inline axis */
}

Logical properties are the pairing skill

.card {
  margin-inline: auto;
  padding-block: 1rem;
  border-inline-start: 4px solid var(--brand);
  inline-size: min(40rem, 100%);
}

Physical left/right CSS for spacing is the #1 RTL bug source.

Bidirectional isolation

<p dir="rtl">
  السعر:
  <span dir="ltr" class="price">$42.00</span>
</p>
.price {
  unicode-bidi: isolate;
  direction: ltr;
}

Keeps numbers and Latin product codes from scrambling under bidi rules.

Interview out-loud

“dir/direction set inline direction (LTR/RTL). writing-mode switches horizontal vs vertical typesetting and redefines block/inline axes. I prefer logical CSS properties so spacing flips correctly, isolate bidi islands for codes and prices, and retest flex/grid when using vertical modes.”

Footguns

  1. Absolute left positioning for “start-side” icons.
  2. Icons that imply direction (chevrons) not mirrored in RTL.
  3. Assuming screenshots of LTR for vertical CJK layouts.
  4. Forgetting form control quirks inside direction: rtl.
  5. Mixing writing-mode on a parent without resetting children that shouldn’t inherit it.

Vertical UI navigation example

.spine-nav {
  writing-mode: vertical-rl;
  display: flex;
  gap: 1rem;
  height: 100%;
  padding: 1rem 0.5rem;
}
.spine-nav a {
  text-orientation: mixed;
  text-decoration: none;
}

Keyboard order still follows DOM order — structure the HTML in the reading order you want, then apply writing-mode for visual presentation. Do not assume visual top equals first tab stop without checking.

Forms in RTL

Native inputs generally flip correctly under dir=rtl, but custom selects, icon adornments, and clear buttons often hard-code left offsets. Audit:

.field-icon {
  inset-inline-start: 0.75rem;
}
.field-input {
  padding-inline-start: 2.25rem;
}

Replace left/padding-left with logical equivalents. Number inputs and OTP fields may stay LTR islands with dir="ltr" for readability.

Further reading

Related guides