Arabic & RTL

Right-to-left, engineered as a first language.

Every competitor ships RTL as a dir attribute and stops there. dev-dga treats Arabic as a first-class language - Hijri calendars, abjad counters, protected letter-joining, mirrored keyboard semantics, and bidi-safe Saudi data, all live below. Flip the direction and watch the whole page respond.

RTL is not a checkbox.

A dir="rtl" flip mirrors boxes. It does not give you an Umm al-Qura calendar, abjad list markers, cursive joining that survives your design tokens, arrow keys that follow the reading order, or a National ID that stays legible inside an Arabic sentence. Those are the details a Saudi government team notices first - so they are the ones this system gets right.

Preview direction
One switch flips every example below - layout, overlays, calendars, and keyboard focus all follow.

Hijri / Umm al-Qura, built in

The DatePicker and DateRangePicker carry an in-calendar Gregorian ↔ Hijri toggle. Switch to Hijri and each day cell shows the Umm al-Qura date with the Gregorian day beneath it, so people cross-reference without leaving the field. The stored value is always a native Date - the Hijri view is display only.

تاريخ الميلاد
بدّل بين الميلادي والهجري داخل التقويم.
فترة التقرير
تواريخ أم القرى، بأرقام لاتينية.
  • Gregorian ↔ Hijri toggleA control inside the popover, on by default. Or pin one system with the calendar prop.
  • Dual-calendar day cellsshowSecondaryCalendar prints the other system’s day number, small, beneath each day.
  • Latin digits, alwaysNumerals render 0-9, never Arabic-Indic - the Saudi government convention for forms and IDs.
  • Locale-aware weekday headerArabic weekday initials read ح ن ث ر خ ج س - real day letters, not a collapsed “ال”.
import { DatePicker } from '@dev-dga/react';// Hijri first, with the Gregorian day shown small under each cell.<DatePicker  label="تاريخ الميلاد"  defaultCalendar="hijri"  showSecondaryCalendar/>;

The weekday-initial fix shipped in @dev-dga 0.12.2. Earlier builds collapsed several Arabic weekdays to “ال”.

Nested lists count in abjad

An ordered List numbers its top level 1-2-3 in both directions. Its nested items are where language shows: Latin a-b-c under LTR, and the Arabic abjad sequence أ-ب-ج-د-ه under RTL - the historical alphanumeric ordering, driven entirely by a CSS @counter-style.

LTR - nested items a-b-c
  1. Eligibility
  2. National ID
  3. Proof of address
  4. Review
  5. Submit
RTL - nested items أ-ب-ج
  1. الأهلية
  2. رقم الهوية
  3. إثبات العنوان
  4. المراجعة
  5. الإرسال

Both lists below use identical markup. Only the direction differs.

import { List, ListItem } from '@dev-dga/react';// Top level counts 1-2-3; nested items count a-b-c (LTR) / أ-ب-ج (RTL).<List variant="ordered" aria-label="خطوات الطلب">  <ListItem>الأهلية</ListItem>  <ListItem level={2}>رقم الهوية</ListItem>  <ListItem level={2}>إثبات العنوان</ListItem>  <ListItem>المراجعة</ListItem></List>;

Cursive joining is protected

Arabic is cursive - its letters connect. The tracking that tightens Latin display type shatters an Arabic word into disconnected glyphs. The system resets letter-spacing to normal under RTL and never uppercases Arabic, so headings and eyebrows stay legible.

Protected - letters joined

الحكومة الرقمية

Letter-spaced - joining broken

الحكومة الرقمية

The second sample adds letter-spacing: 0.25em - exactly what a Latin display utility would do. Never apply it to Arabic.

/* Arabic is cursive. Tracking disconnects the glyphs. */[dir='rtl'] {  letter-spacing: normal; /* reset any Latin tracking */  text-transform: none;   /* never uppercase Arabic  */}

Arrow keys follow the reading order

Keyboard semantics invert with the layout. In Tabs, Slider, Rating, Menubar, and the DatePicker grid, the arrow that moves to the next item points along the reading direction - so under RTL, Left advances and Right goes back. Home and End jump to the first and last item; a roving tabindex keeps a single tab stop.

  • ← →Move between items, inverted under RTL to match reading order.
  • Home EndJump to the first / last item in the group.
  • TabOne stop for the whole group; arrows move within it (roving tabindex).

الأسهم تنقّل بين التبويبات - وتنعكس في اليمين.

الميزانية الشهرية
تقييم الخدمة

Focus a control below and try the arrows - flip the direction and feel them invert.

import { Tabs, TabsList, TabsTrigger, TabsContent } from '@dev-dga/react';// Arrow keys, Home/End and roving focus are handled for you,// and invert automatically under dir="rtl". No key handlers.<Tabs defaultValue="overview">  <TabsList>    <TabsTrigger value="overview">نظرة عامة</TabsTrigger>    <TabsTrigger value="requests">الطلبات</TabsTrigger>  </TabsList>  <TabsContent value="overview">…</TabsContent></Tabs>;

One layout, mirrored by logical properties

Nothing on this page uses left, right, margin-left, or text-align: left. Spacing, borders, and alignment are all written as logical properties - margin-inline, inset-inline-start, text-align: start - so a single dir flip mirrors the entire tree with zero per-component work. The card below is one component; it simply reflects.

Same component, mirrored
سجل تجاريمُعتمَد · الرياض

The @dev-dga component CSS is authored the same way: logical properties for layout, so a dir flip mirrors every box - no per-component RTL branches.

/* Physical: needs a second rule for RTL, easy to forget. */.card { margin-left: 1rem; text-align: left; }/* Logical: mirrors automatically when dir flips. Write it once. */.card { margin-inline-start: 1rem; text-align: start; }

Saudi identifiers stay LTR inside Arabic

A National ID, an IBAN, or a +966 mobile number is left-to-right data - even in an Arabic sentence. Left to the bidi algorithm, the leading + or SA jumps to the wrong end and the number becomes unreadable. Each field pins the value with a native dir="ltr" and the right inputMode, so the label mirrors but the number never scrambles.

+966

Labels and layout follow the page direction; the field values are locked LTR. Sample values shown.

import { TextInput } from '@dev-dga/react';// The value is LTR data even inside an Arabic sentence: pin it.<TextInput  label="رقم الآيبان"  dir="ltr"  inputMode="text"  defaultValue="SA44 2000 0001 2345 6789 1234"/>;

Build your Arabic-first product on a system that already thinks in RTL.