Skip to main content
Back to Blog
Guide
2026-07-13

Testing RTL Layouts with Visual Regression

Test RTL layouts with visual regression by controlling fonts and direction, checking logical alignment, mirrored icons, overflow, mixed text, and baselines.

Testing RTL Layouts with Visual Regression

The Arabic checkout looks plausible until the screenshot diff reveals three asymmetric defects: the coupon icon still points right, a validation bubble is clipped against the left edge, and the totals column remains aligned for English. A global dir="rtl" can reverse flow, but it cannot decide which artwork is directional, whether mixed Latin text needs isolation, or whether a physical CSS offset should have been logical.

Visual regression is valuable here because direction bugs are spatial. It must be paired with semantic and geometric assertions, otherwise a tolerant pixel threshold can approve a layout that is consistently wrong.

Define what should mirror and what should stay fixed

RTL is not a horizontal flip of every pixel. Text direction, flex and grid flow, table columns, margins, padding, and navigation arrows often mirror. Logos, media controls, charts, clocks, mathematical notation, and familiar device icons commonly retain orientation. Product and localization teams should classify assets rather than leaving each developer to guess.

ElementTypical RTL behaviorTest observation
Back navigation chevronMirrors to point toward inline startIcon geometry and accessible name agree
Next/previous paginationPositions and arrows mirror semanticallyCorrect destination after click
Brand logoRemains unchangedPixel region matches approved asset
Progress arrow showing sequenceOften mirrors with flowStages read in localized order
Media play triangleUsually unchangedNo accidental CSS transform
Numeric account IDLTR run inside RTL sentenceDigits and punctuation remain readable
Form label and errorAlign to inline startNeither assumes physical left or right

Create this inventory for the page under test. A screenshot reviewer can then distinguish a real directional defect from an intentional invariant.

Drive direction through the real locale path

Do not test RTL solely by executing document.body.style.direction = 'rtl'. That bypasses routing, translation loading, locale-specific fonts, lang and dir attributes, and server-rendered markup. Enter through the same locale mechanism users receive.

import { expect, test } from '@playwright/test';

test.describe('Arabic checkout', () => {
  test.use({
    locale: 'ar-EG',
    timezoneId: 'Africa/Cairo',
  });

  test('renders the review step in RTL', async ({ page }) => {
    await page.goto('/ar/checkout/review?fixture=visual-order');

    await expect(page.locator('html')).toHaveAttribute('lang', /^ar/);
    await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
    await expect(page.getByRole('heading', { name: 'مراجعة الطلب' })).toBeVisible();
    await expect(page.getByTestId('checkout-review')).toHaveScreenshot(
      'checkout-review-ar.png',
      {
        animations: 'disabled',
        caret: 'hide',
        scale: 'css',
      },
    );
  });
});

Browser locale influences APIs and formatting, but it does not automatically set application direction. The lang and dir assertions prove the product applied semantic document metadata. Prefer the HTML dir attribute over using only CSS direction because direction belongs to content semantics and inherits through the document.

Stabilize fonts before approving pixels

Arabic and Hebrew glyph rendering changes when a preferred web font fails and a platform fallback takes over. Different shaping metrics can reflow every line. Wait for document fonts and use the same browser build, operating system image, viewport, device scale, and font files for baseline generation and comparison.

test.beforeEach(async ({ page }) => {
  await page.route('**/api/checkout/**', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        orderId: 'ORD-2048',
        items: [{ name: 'لوحة مفاتيح', quantity: 1, price: 349.0 }],
        total: 349.0,
      }),
    });
  });
});

async function waitForVisualReadiness(page: import('@playwright/test').Page) {
  await page.evaluate(async () => {
    await document.fonts.ready;
  });
  await expect(page.getByTestId('checkout-skeleton')).toBeHidden();
  await expect(page.getByTestId('order-id')).toHaveText('ORD-2048');
}

Network mocking shown here stabilizes application data, not font delivery. Serve fonts locally in the test build or verify the font resource loads. Mask only data that is genuinely nondeterministic and irrelevant. Masking an entire price or address region can hide the very alignment and mixed-direction bugs the RTL suite exists to catch.

For baseline mechanics and snapshot configuration, consult the Playwright visual snapshots guide.

Prefer logical CSS and test computed geometry

Rules such as margin-left and border-right encode physical sides. Logical properties such as margin-inline-start, padding-inline-end, and inset-inline-start follow writing direction. Visual tests catch many mistakes, while small geometric assertions explain them precisely.

test('summary label starts on the right of its value in Arabic', async ({ page }) => {
  await page.goto('/ar/checkout/review?fixture=visual-order');
  await waitForVisualReadiness(page);

  const label = await page.getByTestId('total-label').boundingBox();
  const value = await page.getByTestId('total-value').boundingBox();
  expect(label).not.toBeNull();
  expect(value).not.toBeNull();
  expect(label!.x).toBeGreaterThan(value!.x);

  await expect(page.getByTestId('totals')).toHaveScreenshot('totals-ar.png');
});

This assertion encodes the page's specific design: the label occupies the inline-start side and the value follows toward inline end. It is more informative than a full-page diff alone. Avoid asserting exact pixel coordinates unless fixed placement is the contract; relative positions survive harmless spacing changes.

Physical rule to reviewLogical counterpartCommon screenshot symptom
margin-leftmargin-inline-startGap stays on wrong side
right: 0inset-inline-end: 0Badge pins to LTR corner
text-align: lefttext-align: startLabels remain left aligned
border-leftborder-inline-startAccent bar fails to mirror
Left/right padding pairInline-start/end paddingIcon and text spacing swaps incorrectly

Physical properties are not forbidden. A decorative element anchored to the physical east edge may intentionally remain there. The review question is whether the side is semantic or physical.

Directional icons need semantic assertions

A screenshot can show a chevron reversed, but it cannot prove clicking it navigates backward. Assert icon treatment and behavior. Prefer distinct RTL assets or a documented CSS transform on a directional icon, while excluding non-directional icons from the mirroring selector.

test('RTL back control points and navigates toward the previous step', async ({ page }) => {
  await page.goto('/ar/checkout/payment?fixture=visual-order');
  const back = page.getByRole('link', { name: 'العودة إلى الشحن' });

  await expect(back).toBeVisible();
  await expect(back.getByTestId('directional-chevron')).toHaveCSS(
    'transform',
    'matrix(-1, 0, 0, 1, 0, 0)',
  );
  await expect(back).toHaveScreenshot('back-to-shipping-ar.png');

  await back.click();
  await expect(page).toHaveURL(//ar/checkout/shipping$/);
});

Computed transform matrices can differ if other transforms are composed, so a product-specific data attribute or asset identity may be a more stable semantic check. Keep the screenshot because it verifies the icon's placement, spacing, and visual direction together.

Mixed-direction text is where shallow RTL checks fail

Arabic sentences often contain phone numbers, emails, URLs, coupon codes, currency, and Latin product names. The Unicode bidirectional algorithm can place punctuation unexpectedly when segments lack isolation. Build fixtures containing combinations that have caused real ambiguity:

  1. Arabic label followed by support@example.test.
  2. Order ID ORD-123-XY between Arabic words.
  3. A positive and negative decimal amount with currency.
  4. Parentheses around a Latin SKU.
  5. User-generated text beginning with an unknown direction.
  6. Arabic text containing an LTR URL with query parameters.

Use <bdi> or equivalent isolation for unknown user content where appropriate. dir="auto" lets the browser determine direction from the first strong character for such values. Do not force every number to RTL.

Take component-level snapshots for these strings. Full-page diffs make a punctuation displacement hard to locate and expensive to review.

Overflow checks complement screenshots

Text clipping can be subtle in a scaled diff. Assert that important containers do not overflow and that controls remain inside the viewport across widths.

test('Arabic address card does not overflow a narrow viewport', async ({ page }) => {
  await page.setViewportSize({ width: 360, height: 800 });
  await page.goto('/ar/account/address?fixture=long-arabic');
  await page.evaluate(async () => document.fonts.ready);

  const overflow = await page.getByTestId('address-card').evaluate((element) => ({
    horizontal: element.scrollWidth > element.clientWidth,
    vertical: element.scrollHeight > element.clientHeight,
  }));
  expect(overflow.horizontal).toBe(false);
  expect(overflow.vertical).toBe(false);
  await expect(page.getByTestId('address-card')).toHaveScreenshot(
    'long-address-ar-360.png',
  );
});

Some components intentionally scroll, such as a data grid. In that case assert the intended scroll axis, visible controls, and initial scroll position rather than a universal no-overflow rule. RTL scrollLeft behavior has browser nuances, so test user-observable navigation rather than hard-coding one raw value across engines.

Baselines should be paired, not derived by flipping

Maintain approved LTR and RTL baselines from actual renders. Do not generate an RTL expectation by mirroring the LTR PNG. Text glyphs, asset exceptions, localized length, number formatting, and component order differ.

Baseline scopeStrengthMaintenance cost
Whole page at key breakpointFinds interaction among componentsLarge diffs from small content changes
Stable section or componentLocalizes directional defectsMay miss page-level alignment
Icon control cropPrecise asset and spacing reviewToo narrow for surrounding flow
LTR and RTL geometry assertionsExplains intended relationshipDoes not cover paint and typography

Use a small portfolio: one page snapshot, focused snapshots for risky components, and semantic assertions. More snapshots are not automatically more coverage if reviewers approve noise without inspection.

Keep screenshot tolerance honest

Playwright waits for two consecutive stable screenshots before comparison and disables animations by default. Configure thresholds narrowly enough to expose real changes. Pixel-difference tolerances are appropriate for minor rasterization variation, not for hiding a permanently misplaced divider.

A stylePath can hide cursors or third-party dynamic widgets, but every override changes what is under test. Store the stylesheet beside visual tests and review it like test code. If it hides timestamps, verify that it does not also hide their alignment container.

Baseline updates must happen in the canonical container or runner image. Review RTL changes with someone able to assess the language and interaction semantics. A green diff produced by blindly accepting screenshots is weaker than no visual gate because it creates false confidence.

Responsive matrices should target directional risk

Do not multiply every locale, browser, viewport, and theme without a reason. Choose combinations from layout risk:

VariantDefect class targeted
360 px ArabicWrapping, clipped actions, off-canvas navigation
Desktop HebrewDense tables and mixed numeric columns
WebKit RTLEngine-specific shaping and scroll behavior
High contrast or dark RTLDirectional assets and focus indicators
200% zoom equivalent viewportReflow and logical spacing

Keep one canonical environment for strict pixel baselines. Use additional engines for semantic, overflow, and selective component snapshots if raster differences make shared baselines costly.

The internationalization testing guide covers translations, pluralization, dates, collation, and locale input beyond visual direction.

Triage RTL diffs by layer

When a diff appears, check document state first: correct locale, lang, and dir. Then verify fonts and fixture data. Next classify the region as flow, alignment, asset mirroring, bidi isolation, overflow, or unrelated nondeterminism. Use Playwright trace and DOM inspection to connect pixels to computed CSS.

An image diff showing the whole panel shifted can stem from one physical margin on the parent. Fix the root logical property instead of updating child offsets. If only glyph edges differ across CI, compare font network responses and runner image before increasing tolerance.

A release-ready RTL visual suite

For each critical journey, include the real RTL locale route, deterministic representative content, loaded target fonts, semantic document attributes, selected visual baselines, relative geometry for key relationships, behavior checks for directional controls, mixed-script fixtures, and overflow cases at narrow widths. Run it in a pinned environment and require human review for baseline changes.

Visual regression then serves its proper role: detecting spatial consequences that DOM assertions miss, while semantic checks explain whether the mirrored interface still works.

Forms mix directions inside one component

Arabic forms still contain LTR email addresses, card numbers, and coupon codes. Assert field-level direction, validation placement, reveal-icon position, and focus outline rather than assuming every descendant inherits RTL.

test('email remains readable in the RTL profile form', async ({ page }) => {
  await page.goto('/ar/account/profile?fixture=mixed-fields');
  const email = page.getByLabel('البريد الإلكتروني');
  await expect(email).toHaveValue('reader@example.test');
  await expect(email).toHaveCSS('direction', 'ltr');
  await email.focus();
  await expect(page.getByTestId('email-field')).toHaveScreenshot(
    'email-field-focused-ar.png',
    { caret: 'hide' },
  );
});

Whether the field aligns physically left or at inline start is a design decision, but character order must remain intelligible. Include the longest supported localized error message.

Tables need approved column semantics

The dir attribute can reverse table columns. Some analytic products intentionally keep metric order stable while localizing labels. Capture a focused baseline and assert header associations with roles.

Table riskVisual checkBehavioral check
Reversed columnsApproved RTL sequenceCells retain correct headers
Numeric alignmentDecimal columns line upValues sort numerically
Sticky identifierCorrect inline edge sticksHorizontal scroll keeps it visible
Sort indicatorArrow avoids heading overlapClick updates aria-sort
Wide contentEdge shadow appears correctlyKeyboard reaches hidden cells

Do not assume mirroring the whole grid is correct. Domain conventions may outweigh simple flow reversal.

Keyboard order is not visible in pixels

CSS can reorder children without changing DOM focus order. Walk controls with Tab and assert focus through accessible roles. Task order does not always correspond to monotonically decreasing x-coordinates, especially in dialogs and mixed-direction forms.

Inspect focus-ring clipping at both inline edges. An outline tuned for an LTR drawer can disappear under an RTL overflow container. Keep keyboard behavior as the oracle and add a focused snapshot for paint.

Theme variants can choose different SVG assets or remove background arrows in forced colors. Test accessible naming first, then use targeted theme snapshots for components whose directional art actually changes. Avoid multiplying every page baseline across every theme without a specific directional risk.

Test overlays at both inline edges

Tooltips, menus, date pickers, and validation popovers frequently use physical positioning from an anchoring library. Open them near the right and left viewport edges in RTL, then assert they remain visible and point to the triggering control. A closed-state page screenshot cannot expose collision and fallback-placement defects.

Dialogs introduce their own direction boundary. The page may be RTL while an embedded code sample remains LTR. Verify the dialog inherits the intended dir, its close button occupies the approved side, focus enters and returns correctly, and long localized actions wrap without reversing priority.

Compare meaning, not pixel symmetry

An RTL screenshot does not need to be geometrically symmetric with English. Translation length changes wrapping, Arabic fonts have different ascenders, and localized dates change width. Review whether hierarchy, relationships, and task flow are preserved. Exact mirroring is expected only for selected spatial structures.

When a diff is caused by an approved translation update, examine overflow and adjacent controls before updating the baseline. A legitimate text change can reveal a real layout defect. Record why the baseline changed in review so future investigators know whether an asymmetry was intentional.

Frequently Asked Questions

Can I create the RTL baseline by flipping the LTR screenshot?

No. RTL rendering includes different text, shaping, punctuation, component order, and assets that intentionally do not mirror. Capture an actual RTL render through the application's locale path.

Should every icon be mirrored in an RTL interface?

No. Mirror icons that encode horizontal direction, such as back and forward in a flow. Logos, media controls, clocks, and many universal symbols usually remain fixed. Maintain a product-reviewed icon classification.

Why do RTL screenshots differ only in CI?

The usual causes are missing or different fonts, a different browser build, viewport or device-scale mismatch, locale data, and animations or late content. Pin the environment and wait for document.fonts.ready plus application readiness.

Is dir="rtl" enough to test Arabic layout?

It is necessary but not sufficient. Test localized content, logical CSS, mixed-direction strings, directional assets, overflow, focus order, and control behavior. Enter via the real locale selection so server and client setup are included.

How much screenshot difference should an RTL test allow?

Use the smallest tolerance compatible with stable rendering in the pinned environment. Derive it from observed antialiasing noise, not from the size of a current failure. A tolerance should never be used to approve layout displacement.

Testing RTL Layouts with Visual Regression | QASkills.sh