Founder & Lead Engineer, RAITHub
To QA an Arabic right-to-left web app for Saudi Arabia and the GCC, test every screen in both directions against a checklist of 20 checks: direction set in markup, logical CSS instead of left and right, a deliberate numeral choice, mirrored directional icons only, isolated mixed-direction strings, and Playwright screenshot tests per locale. Many RTL bugs are layout regressions that a screenshot diff catches.
This checklist is for product teams and QA engineers shipping a bilingual Arabic and English web app. RTL (right-to-left) means the page reads and lays out from right to left; bidi (bidirectional) means one page mixes both directions, as almost every Arabic product does with brand names, prices and order numbers. This is engineering guidance: RAITHub builds and tests RTL interfaces, but it delivers in English and has no published Arabic product, so the Arabic copy and its linguistic review should come from a native speaker on your team or a translator.
Why is Arabic RTL more than translating the text?
Because direction changes the layout, not just the words. When a page switches to Arabic, the sidebar moves, the back arrow points the other way, the progress bar fills from the right, and every sentence that contains an English word or a number becomes a bidirectional string that the browser has to order correctly.
The W3C's guidance is to set direction in the markup, with dir="rtl" on the html element, rather than in CSS, and to use dir="auto" for text whose direction you do not know in advance (W3C: structural markup and right-to-left text). For layout, CSS logical properties describe spacing and position relative to the reading direction, so margin-inline-start becomes a right margin in Arabic without a second stylesheet (MDN: CSS logical properties). Many RTL defects come from code that ignores one of those two ideas.
What should an Arabic RTL QA checklist cover?
Twenty checks across markup, layout, content, input and automation. Run the manual ones once per new screen and automate the rest.
| # | Area | Check | How to test |
|---|---|---|---|
| 1 | Markup | Arabic pages have lang="ar" and dir="rtl" on the html element | Playwright attribute assertion per locale |
| 2 | Markup | Direction comes from the dir attribute, not the CSS direction property | Search the stylesheets; review |
| 3 | Layout | Margins and padding use logical properties such as margin-inline-start and padding-inline | Lint rule (below) |
| 4 | Layout | Positioned elements (badges, close buttons, dropdowns, toasts) use inset-inline-start and inset-inline-end | Screenshots of menus, modals and toasts |
| 5 | Layout | Text alignment uses start and end, not left and right | Lint rule; screenshots |
| 6 | Layout | Flex and grid rows follow the document direction; no row-reverse added just for Arabic | Screenshots of headers, nav bars and cards |
| 7 | Layout | No horizontal scroll appears in Arabic that does not appear in English | Automated overflow check (below) |
| 8 | Icons | Directional icons are mirrored: back and forward arrows, chevrons, breadcrumb separators, "next" and "reply" | Screenshots; component test under [dir="rtl"] |
| 9 | Icons | Non-directional icons are not mirrored: logos, checkmarks, clocks, media play buttons | Screenshots; review against the icon list |
| 10 | Numbers | Western (0 to 9) or Arabic-Indic digits are chosen on purpose and set explicitly, not left to the browser default | Unit test on the formatter (below) |
| 11 | Dates | The calendar (Gregorian or Hijri) is chosen on purpose and pinned in the formatter | Unit test on resolvedOptions().calendar |
| 12 | Money | SAR and AED amounts format the same way on every screen, email and PDF | Unit test on the money formatter; screenshots |
| 13 | Mixed text | User-written and interpolated text is isolated with dir="auto" or a bdi element | Fixtures mixing Arabic, English and numbers |
| 14 | Mixed text | LTR islands stay LTR: phone numbers, emails, URLs, IBANs, order and SKU codes | Fixtures; inputs with dir="ltr" |
| 15 | Forms | Free-text inputs use dir="auto"; labels, errors and helper text sit on the correct side | Screenshots of empty, filled and error states |
| 16 | Overflow | Truncation and ellipsis appear at the end of the text in both directions | Long-string fixtures in screenshots |
| 17 | Type | An Arabic web font loads with a sensible fallback, line height leaves room for diacritics, and no letter-spacing is applied to Arabic text | Screenshots; review |
| 18 | Data display | Table column order, chart axes, carousels and swipe gestures follow the reading direction you decided on | Screenshots; manual swipe on a phone |
| 19 | Keyboard | Arrow keys in tabs, sliders and menus move in the visual direction; focus order matches visual order | Playwright keyboard test |
| 20 | Outside the app | Transactional emails, PDFs (invoices, receipts) and SMS templates are RTL too | Render templates to HTML and screenshot them |
Keep the list in the repository next to the tests, and link each row to the test that covers it. A row with no automated test is a row someone has to remember.
How should numerals, dates and currency work in Saudi and GCC apps?
Choose each format on purpose, write it down, and pin it in code so it does not change with the browser. Arabic-language locales can format numbers with Arabic-Indic digits (٠ to ٩) or Western digits, and CLDR-based runtimes may pick a different default depending on the locale and version. Dates have the same trap: for ar-SA, the runtime default calendar can be the Hijri calendar rather than the Gregorian one. Decide with your product owner which your customers expect on each screen.
JavaScript's Intl APIs accept the choice as a Unicode extension on the locale: -u-nu-latn for Western digits, -u-nu-arab for Arabic-Indic, and -u-ca-gregory for the Gregorian calendar (MDN: Intl.Locale numberingSystem). Put the formatters in one module and test that module:
// format.ts: every number and date on the site goes through here
export type Digits = 'latn' | 'arab'
export const formatNumber = (n: number, digits: Digits) =>
new Intl.NumberFormat('ar-SA-u-nu-' + digits).format(n)
export const formatDate = (d: Date) =>
new Intl.DateTimeFormat('ar-SA-u-ca-gregory-nu-latn', {
dateStyle: 'medium',
timeZone: 'Asia/Riyadh',
}).format(d)
// format.test.ts
import { describe, it, expect } from 'vitest'
import { formatNumber, formatDate } from './format'
describe('Arabic formatting is pinned, not left to the runtime', () => {
it('uses the digits we chose', () => {
expect(formatNumber(1234, 'latn')).not.toMatch(/[٠-٩]/)
expect(formatNumber(1234, 'arab')).toMatch(/[٠-٩]/)
})
it('uses the Gregorian calendar', () => {
const f = new Intl.DateTimeFormat('ar-SA-u-ca-gregory-nu-latn')
expect(f.resolvedOptions().calendar).toBe('gregory')
expect(formatDate(new Date('2027-01-15T12:00:00Z'))).toContain('2027')
})
})
Two details save debugging time. Arabic output from Intl can contain invisible direction marks, so assert on content with toContain or a character class rather than an exact string. And prices need the same treatment: format SAR and AED through one money formatter that takes integer minor units (halalas and fils), so a receipt email never shows a different format from the checkout page.
How do you handle mixed Arabic and English in one string?
Isolate every piece whose direction differs from its surroundings. An Arabic sentence such as "your order ORD-1042 for 2 items has shipped" contains an LTR code and a number; without isolation, the browser's bidi algorithm can move the punctuation or the number to the wrong side.
The W3C recommends wrapping each opposite-direction phrase in markup with a dir attribute, and using dir="auto" or the bdi element when you cannot know the direction in advance, such as a user's name or a product title. Where markup is not possible, such as a title attribute, it recommends the Unicode isolate characters RLI, LRI and PDI (W3C: inline markup and bidirectional text). In a template:
<!-- Unknown direction: let the browser decide from the first strong character -->
<p><bdi>{customerName}</bdi> ...</p>
<!-- Known LTR islands inside Arabic text -->
<span dir="ltr">ORD-1042</span>
<a dir="ltr" href="mailto:{email}">{email}</a>
<!-- Phone and email inputs stay LTR on Arabic pages -->
<input type="tel" dir="ltr" autocomplete="tel">
<input type="email" dir="ltr" autocomplete="email">
<!-- Free text: follows what the user types -->
<textarea dir="auto"></textarea>
Build a fixture set for these cases and use it in every screenshot test: an Arabic product title with an English brand name, a very long Arabic title, an English-only name on an Arabic page, a Saudi mobile number, an email address, and an order code. Brackets and parentheses mirror automatically with the text direction, the W3C notes, so do not "fix" them by hand.
Which icons should be mirrored in a right-to-left layout?
Icons that point along the reading direction, and nothing else. The test is whether the icon means "forwards" or "backwards" in the flow of the page.
- Mirror: back and forward arrows, chevrons in lists and breadcrumbs, "next" and "previous", reply and forward in messaging, undo and redo, progress indicators that fill along the line, and icons showing text alignment or indentation.
- Do not mirror: logos and brand marks, checkmarks, clocks and timers (they turn clockwise in every language), media play and fast-forward buttons, a phone handset, and anything that depicts a real object.
- Decide once and record it: icons such as a search magnifier or a shopping cart, where conventions vary. Put the decision in your icon component, not in each screen.
A single rule in the icon component handles the first group, for example flipping icons marked as directional with transform: scaleX(-1) under [dir="rtl"]. Test that component in both directions, and screenshot one screen that uses each group.
How do you automate RTL checks with Playwright?
Run the same end-to-end tests twice, once per locale, and compare screenshots against approved baselines. Playwright lets you set locale per project (Playwright: emulation) and compare pages with toHaveScreenshot, writing a baseline on the first run and updating it with --update-snapshots (Playwright: visual comparisons).
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
expect: { toHaveScreenshot: { maxDiffPixels: 100 } },
projects: [
{ name: 'en', use: { ...devices['Desktop Chrome'], locale: 'en-US' } },
{ name: 'ar', use: { ...devices['Desktop Chrome'], locale: 'ar-SA' } },
],
})
// tests/rtl.spec.ts
import { test, expect } from '@playwright/test'
// Adjust to your routing; here each locale lives under /en and /ar.
const paths = ['/', '/pricing', '/checkout', '/account/orders']
for (const path of paths) {
test('layout holds on ' + path, async ({ page }, testInfo) => {
const lang = testInfo.project.name // 'en' or 'ar'
await page.goto('/' + lang + path)
// Checklist 1: direction comes from the markup.
const html = page.locator('html')
await expect(html).toHaveAttribute('lang', lang)
await expect(html).toHaveAttribute('dir', lang === 'ar' ? 'rtl' : 'ltr')
// Checklist 7: nothing pushes the page sideways.
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
)
expect(overflow).toBeLessThanOrEqual(0)
// Everything else visual: diff against this locale's approved baseline.
await expect(page).toHaveScreenshot({ fullPage: true, animations: 'disabled' })
})
}
Each project gets its own baseline images, so the Arabic and English screens are compared only with themselves. Playwright warns that rendering varies with the operating system, browser version, settings and hardware, so generate and compare baselines in the same environment, typically one pinned container image in CI. Load your fixtures before each screenshot, and mask anything that changes on every run, such as timestamps.
For choosing a runner, see Playwright vs Cypress; if screenshot tests start failing at random, the causes and fixes are in fixing flaky end-to-end tests.
How do you stop left and right creeping back into the CSS?
With a lint rule that fails the build. Stylelint's built-in property-disallowed-list and declaration-property-value-disallowed-list rules can block physical properties and values:
{
"rules": {
"property-disallowed-list": [
"margin-left", "margin-right", "padding-left", "padding-right",
"border-left", "border-right", "left", "right"
],
"declaration-property-value-disallowed-list": {
"text-align": ["left", "right"],
"float": ["left", "right"]
}
}
}
There are genuine exceptions, such as a chart that must stay left-to-right, so allow a reviewed inline disable comment with a reason rather than loosening the rule. If you use a utility-class framework, check that its RTL or logical variants are what your components use, and add a search for physical utility classes to the same CI step.
Run the lint and the two-locale Playwright suite on every pull request, as described in regression testing on every deploy. A layout that was right at launch drifts one component at a time.
Why RAITHub for this?
- QA-first by default. TheSkinProof, the founder's own marketplace venture, runs 750+ automated tests; PropDesk runs 1,024; this website runs 400+. An RTL suite is the same discipline applied to a second direction. The approach is in how RAITHub tests software.
- RTL as engineering, stated plainly. Logical CSS, bidi isolation, pinned formatters and per-locale screenshot baselines are code and tests RAITHub writes. The Arabic words and their review stay with your team or a translator, because RAITHub delivers in English.
- Gulf-friendly hours. About 6 shared working hours with Riyadh and 7 with Dubai, and a working week agreed with you, so a Sunday to Thursday week can be accommodated.
- Fixed scope. A free 15-minute technical audit, then a written quote for an RTL audit, a test suite, or fixes. You own the code and the tests.
When you don't need us
- You need Arabic linguistic QA: grammar, tone, terminology and translation review. That needs a native Arabic speaker, not an engineering team.
- Your site is a hosted theme with built-in RTL support and no custom components. Test it by hand against the checklist above.
- You need native iOS or Android RTL testing. RAITHub does not offer mobile-app services; this checklist covers web apps and PWAs.
- You need a certified accessibility audit. Choose a specialist auditor; RTL checks complement accessibility testing but do not replace it.
To add these checks to your pipeline, see the QA and test automation service, then book the free 15-minute technical audit. Bring two or three screens that look wrong in Arabic and the list of locales you support. For everything else to check before launch, use the pre-launch QA checklist.
Last reviewed: 30 September 2026. W3C, MDN and Playwright documentation checked on 30 September 2026.
Frequently asked questions
How do I test an Arabic RTL website?
Run every screen in both Arabic and English against a checklist: direction in markup, logical CSS, numerals and dates, mirrored icons, mixed-direction text, forms and emails. Automate it with a Playwright project per locale, screenshot baselines, and an overflow check, plus a lint rule that blocks left and right in CSS.
Should I use dir="rtl" or the CSS direction property?
Use the dir attribute. The W3C recommends setting direction in markup, with dir="rtl" on the html element for Arabic pages and dir="auto" for text whose direction you do not know in advance.
Should an Arabic web app show Western or Arabic-Indic numerals?
It is a product decision, and both are used. What matters is choosing on purpose and pinning it in code with a locale extension such as -u-nu-latn or -u-nu-arab, so the browser default cannot change it.
Which icons should flip in right-to-left layouts?
Icons that point along the reading direction: back and forward arrows, chevrons, next and previous, reply and progress. Logos, checkmarks, clocks and media play buttons keep their original direction.
How do I stop Arabic text with English words from scrambling?
Isolate each opposite-direction piece. Wrap known LTR items such as order codes, emails and phone numbers in an element with dir="ltr", and use dir="auto" or a bdi element for names and user-written text.
Can Playwright screenshot tests catch RTL layout bugs?
Yes, most visual ones. Give each locale its own Playwright project so each has its own baselines, generate and compare them in one pinned environment, and add attribute and overflow assertions for the bugs a screenshot might hide.
Does RAITHub translate content into Arabic?
No. RAITHub delivers in English. It builds and tests the Arabic RTL interface; the Arabic copy and its review should come from your team or a translator.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.