Rtl Layout
Try in playground →opens this recipe’s markup and SCSS, live-compiled
Ship a right-to-left (Arabic, Hebrew) layout with cia — what works for free from logical properties, what needs a dir="rtl" override, and how to flip directional icons.
Use this when
You're shipping a page or app to a right-to-left locale — Arabic, Hebrew,
Farsi, Urdu — and need to know what cia already handles and what you still
have to do. cia has used CSS logical properties (margin-inline-start
instead of margin-left, text-align: start instead of left) since v0.7,
so most layout mirrors automatically the moment you set dir="rtl". A
handful of things — directional icons, background-position, transform —
have no logical-property equivalent and need an explicit override; this
recipe is the list of which ones and how cia handles each.
This recipe is about layout direction, not translation or locale formatting (dates, numbers, currency, pluralization) — that's the i18n recipes once shipped. It also doesn't cover BiDi text rendering edge cases (mixed Arabic/Latin in one string) beyond flagging them in Pitfalls below — browsers handle the rendering; cia doesn't add to or subtract from that.
Structure (raw HTML)
Set direction once, at the root. Everything under it inherits.
<html lang="ar" dir="rtl">
<body>
<!-- cia components render mirrored automatically — no per-component
dir prop, no RTL variant class. -->
</body>
</html>
A single section can override the page's direction locally — useful for an embedded LTR code sample inside an otherwise-RTL page, or vice versa:
<div dir="ltr">
<!-- e.g. a code block, a product SKU, a URL — content that stays LTR
regardless of the surrounding page direction -->
</div>
Styling (cia mixins)
What "just works" — no code change, no RTL variant. Because cia's own
source uses logical properties throughout — audited and CI-gated on every
PR, so drift can't creep back in — every mixin below mirrors correctly
under dir="rtl" with zero extra CSS:
- Spacing:
cia.pad,cia.pad-asym,cia.space,cia.grid— all emitmargin-inline-*/padding-inline-*, never-left/-right. - Text:
cia.type, any mixin touchingtext-align— readsstart/end, notleft/right. - Borders: component mixins with a directional accent border (the
breadcrumb separator in
cia.breadcrumb, the avatar-stack overlap incia.avatar-group, the accordion chevron) useborder-inline-*/margin-inline-*and mirror correctly. - The Tier-1 utility classes ship both a physical set (
cia-ml-*,cia-mr-*,cia-text-left,cia-text-right,cia-border-l,cia-border-r,cia-left-0,cia-right-0) for when you want a specific visual side regardless of direction, and a logical set (cia-ms-*/cia-me-*,cia-mi-*/cia-pi-*,cia-text-start/cia-text-end,cia-border-s/cia-border-e,cia-start-0/cia-end-0) that mirrors. Reach for the logical set unless you specifically want a fixed side.
@use 'css-is-awesome/api' as cia;
// Mirrors automatically under dir="rtl" — no override needed.
.sidebar {
@include cia.pad(4);
border-inline-end: 1px solid var(--border-default);
}
What needs an explicit [dir="rtl"] override. Two CSS features have no
logical-property equivalent with reliable browser support: background- position and transform. cia's own select and switch mixins hit both —
read their source (scss/components/_forms.scss) as the reference pattern:
// A select's dropdown chevron — background-position can't use a logical
// keyword, so the LTR value gets an explicit RTL flip.
.custom-select {
background-position: right 0.5rem center; // LTR default
padding-inline-end: 2rem; // this one DOES flip on its own
[dir="rtl"] & {
background-position: left 0.5rem center;
}
}
// A toggle knob's resting position IS logical (mirrors on its own); the
// SLIDE distance is a transform, which isn't direction-aware, so only the
// checked-state transform needs the override.
.switch::before {
inset-inline-start: 1px; // mirrors automatically
}
.switch:checked::before {
transform: translateX(1.25rem);
}
[dir="rtl"] .switch:checked::before {
transform: translateX(-1.25rem); // same distance, opposite direction
}
Icon-flipping (chevrons, arrows)
Not every icon should flip in RTL — that's the actual hard part. A directional icon (a "next" chevron, a back arrow, an undo icon whose curve implies a reading direction) should flip, because it encodes "forward" or "backward" relative to reading order. A non-directional icon (a logo, a brand mark, a checkmark, a search icon) must never flip — it has no direction, and mirroring it looks broken or, for a logo, wrong.
Two flip mechanisms, pick based on the icon's shape:
/* Mechanism 1 — transform, for a whole-icon flip. Cheapest; use for any
icon that's symmetric enough that a horizontal mirror reads correctly
(most chevrons, arrows). */
[dir="rtl"] .icon-flip {
transform: scaleX(-1);
}
<!-- Mechanism 2 — SVG-internal flip, for an icon whose flip needs to
happen INSIDE the shape (asymmetric glyphs, or when you're already
applying a transform to the icon for something else and don't want
them to compound). Flip the path data itself, or wrap it: -->
<svg viewBox="0 0 24 24" aria-hidden="true">
<g class="icon-flip-inner">
<path d="M9 6l6 6-6 6" />
</g>
</svg>
<style>
[dir="rtl"] .icon-flip-inner {
transform-box: fill-box;
transform-origin: center;
transform: scaleX(-1);
}
</style>
cia ships no built-in "flip this icon" mixin — the decision of which icons
flip is a product/content decision, not something a library can infer from
an SVG's shape. Mark the ones that should with .icon-flip (or your own
class) and apply mechanism 1 above; leave everything else untouched.
Interactivity
Nothing here needs JavaScript. Direction is a CSS/HTML concern —
dir="rtl" on <html> (or any ancestor) is enough; no script sets or reads
it for layout purposes. If your app supports a runtime locale switcher, the
only JS involved is setting the dir attribute (and lang) when the user
changes locale — no re-render of cia-styled components is needed, since
they all read direction live from the cascade.
A11y checklist
-
<html lang="...">matches the actual content locale, anddir="rtl"is set alongside it — screen readers uselangto select pronunciation rules, and get it wrong silently if it's missing or wrong (WCAG 2.2 SC 3.1.1 Language of Page) - Mixed-direction content (a UI string in Arabic containing an English
product name, a phone number) gets
dir="auto"on that specific element so the browser's Unicode bidi algorithm can resolve it, rather than forcing the wrong base direction (Unicode Bidirectional Algorithm) - A flipped directional icon keeps the same
aria-label/accessible name — flipping is visual only, the meaning ("next", "back") is unchanged (WCAG 2.2 SC 1.1.1 Non-text Content) - Focus order still matches visual reading order after the direction
flip — logical properties handle visual position, but confirm tab order
wasn't hand-coded assuming LTR (e.g. a hardcoded
tabindexsequence) (WCAG 2.2 SC 2.4.3 Focus Order)
Framework examples
Every framework does the same thing: set dir (and lang) on the root
element the app controls, and let cia's logical properties do the rest —
there is no cia-specific RTL API to call.
React
export function App({ locale, children }: { locale: string; children: React.ReactNode }) {
const dir = ["ar", "he", "fa", "ur"].includes(locale) ? "rtl" : "ltr";
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}
Vue
<template>
<div :dir="dir" :lang="locale">
<slot />
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{ locale: string }>();
const RTL_LOCALES = ["ar", "he", "fa", "ur"];
const dir = computed(() => (RTL_LOCALES.includes(props.locale) ? "rtl" : "ltr"));
</script>
Svelte
<script lang="ts">
export let locale: string;
const RTL_LOCALES = ["ar", "he", "fa", "ur"];
$: dir = RTL_LOCALES.includes(locale) ? "rtl" : "ltr";
</script>
<div {dir} lang={locale}>
<slot />
</div>
Vanilla
<script>
const RTL_LOCALES = ["ar", "he", "fa", "ur"];
const locale = document.documentElement.lang || "en";
document.documentElement.dir = RTL_LOCALES.includes(locale) ? "rtl" : "ltr";
</script>
Pitfalls
- Numbers don't flip. Arabic-script text runs right-to-left, but Arabic
numerals (0-9) — used in both English and Modern Standard Arabic — still
read left-to-right within themselves inside an RTL string. Don't apply
direction: rtlto a number or date by hand; let the Unicode bidi algorithm place it (it already does this correctly for plain text — the bug only shows up if you've forcedunicode-bidi: bidi-overridesomewhere, which cia never does). - English brand names inside Arabic sentences. "استخدم GitHub للنشر"
(roughly "Use GitHub to deploy") mixes an LTR brand name into an RTL
sentence. The browser's bidi algorithm generally gets this right on its
own; wrap the Latin fragment in
<span dir="ltr">only if you observe it rendering wrong (extra punctuation reordering is the usual symptom). - Mixed-direction user input. A search box or comment field where a
user might type Arabic, English, or both needs
dir="auto"on the<input>/<textarea>itself — without it, the field's base direction is fixed to the page's and mixed input can render with punctuation and trailing Latin words in the wrong visual order. - A hardcoded physical value slipping into your OWN CSS. cia's source
is logical-property-clean (audited, see above), but your own component
CSS isn't audited by cia. Reach for
cia-ms-*/cia-pe-*/cia-text-startover hand-writtenmargin-left/text-align: leftin your own styles.
Related recipes
print-to-pdf— print direction inherits fromdirthe same way screen layout does; no separate RTL print concern.i18n-date-formatting,i18n-number-currency,i18n-pluralization— locale formatting (dates, numbers, plurals), the concern this recipe explicitly doesn't cover. Icon-flipping and layout direction (this recipe) vs. formatting (those three) is the full i18n picture.