How to Test Right-to-Left Website Layouts with Playwright Screenshots
Build stable Playwright screenshot tests for RTL pages. Set locale and direction explicitly, control rendering differences, and review visual baselines with confidence.
Use Playwright Test’s toHaveScreenshot() to capture an RTL page and compare it with a reviewed baseline. Make the application enter its real RTL locale and state explicitly before the assertion, and run baseline creation and comparisons in a consistent browser and host environment. A screenshot checks rendered pixels; it does not prove that the correct locale, direction, interaction, or accessible structure is configured.
This guide uses TypeScript and Playwright Test. Replace the example locale route and readiness condition with the localization mechanism your application actually uses.
1. Set up Playwright Test
Install Playwright Test and its browser if your project does not already have them:
npm init playwright@latest
In the setup prompts, choose TypeScript if you want to use the examples below. The generated project includes a test configuration and browser installation steps. If Playwright Test is already installed, keep your existing setup.
Configure a stable base URL and viewport in playwright.config.ts. This is a minimal example; retain any existing projects, reporters, or settings your suite needs:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://127.0.0.1:3000',
viewport: { width: 1280, height: 800 },
...devices['Desktop Chrome'],
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});
Install the browser version used by the project with npx playwright install chromium when needed. Pin the Playwright version through your package lockfile so local and CI runs use the same browser build.
2. Write an explicit RTL screenshot test
The following test establishes the route, viewport, and a readiness condition before saving or comparing a named screenshot. The ?locale=ar route is illustrative: use your app’s supported locale, test fixture, or account setting instead.
import { test, expect } from '@playwright/test';
test('Arabic landing page renders in RTL', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('/?locale=ar');
// Replace this with an app-specific signal that locale and page data are ready.
await expect(page.getByRole('main')).toBeVisible();
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
await expect(page).toHaveScreenshot('landing-ar-rtl.png');
});
Use a meaningful screenshot name that identifies the route, locale, and state. If your product supports Hebrew and Arabic, or both a closed and open navigation menu, make separate scenarios and baselines where their appearance should differ. Tests should establish their own prerequisites rather than rely on the order in which other tests ran.
3. Create and review the baseline
Run the test with npx playwright test. On its first run, Playwright reports that the expected screenshot is missing and creates an actual image for review. Inspect the captured page and its RTL-specific details, then generate the expected baseline using your team’s normal snapshot workflow. Commit the reviewed expected image alongside the test.
On later runs, Playwright compares the current capture against that baseline and reports a diff when they differ. Review the actual image, expected image, and diff to determine whether the change is an intentional design update, an application regression, or rendering variation.
4. Test the RTL behavior that matters
A visual snapshot is useful for appearance, but pair it with assertions that explain the required behavior. For example:
- Assert that the intended locale’s heading or content is present.
- Assert that a navigation menu opens before taking a screenshot of its open state.
- Check the expected accessible roles and names when navigation or form structure matters.
- Include mixed RTL and Latin text, numbers, or punctuation if those combinations occur in real content.
- Inspect alignment, navigation placement, directional icons, form labels, and controls at the viewports your product supports.
These are test-design choices, not special Playwright requirements. Test the actual locale and interface states your application promises. A screenshot alone cannot explain why a visual detail is wrong or confirm that an interaction works.
Check accessible structure separately
When the requirement includes accessible roles, names, or hierarchy, add an ARIA snapshot assertion as a separate check. It compares the accessible tree with a template and complements the screenshot comparison:
test('RTL navigation has the expected accessible structure', async ({ page }) => {
await page.goto('/?locale=ar');
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`
- navigation:
- link "Home"
- link "Products"
`);
});
Use names and structure that match your application’s accessible output. See Playwright’s official ARIA snapshot documentation for the matcher and template syntax.
5. Keep captures stable and comparisons meaningful
Visual output can vary with the host operating system, browser version, browser settings, hardware, power source, and headless mode. Generate and compare baselines in the same environment where practical. For CI, use a consistent image and browser installation, and avoid mixing snapshots produced on one platform with comparisons from another unless that difference is intentional.
Playwright’s screenshot assertion waits for two consecutive screenshots to match before comparing the result with the expected image. That helps catch a changing render, but it does not make application data or state deterministic. Prepare the test data, wait for the relevant content, and avoid time-dependent or randomly changing content where possible.
Animations, live timestamps, rotating content, remote data, and hover styles can create noisy diffs. Make the pointer position deterministic; hover styles present at capture time are included. If a region is genuinely volatile and is outside the behavior being tested, use Playwright’s screenshot styling or masking options narrowly. Do not hide a region whose appearance is part of the requirement.
6. Tune screenshot comparison options carefully
toHaveScreenshot() accepts options for naming, dimensions, timing, and comparison sensitivity. The most useful choices for RTL coverage include:
| Option | Use | Guidance |
|---|---|---|
| Screenshot name | Separate locale, route, and UI state baselines. | Use descriptive names such as checkout-ar-mobile.png. |
threshold |
Set the acceptable perceived color difference per pixel. | The documented default is 0.2. Change it only after reviewing actual diffs. |
maxDiffPixels |
Limit the number of pixels allowed to differ. | It is unset by default. A small, justified allowance can help with known rendering variation. |
maxDiffPixelRatio |
Limit differences as a share of the image. | Use when a proportional limit better fits the screenshot size; avoid setting competing limits without a clear reason. |
fullPage |
Capture beyond the viewport when the full layout is the requirement. | Use a viewport screenshot for responsive above-the-fold checks and a full-page capture for long-page layout. |
animations |
Control animations during capture. | Disable or fast-forward animations only when motion is not the behavior under test. |
stylePath |
Apply a stylesheet during screenshot capture. | Use narrowly to suppress known volatile content without masking RTL behavior. |
Example with an explicit pixel tolerance:
await expect(page).toHaveScreenshot('catalog-hebrew.png', {
threshold: 0.2,
maxDiffPixels: 30,
fullPage: true,
});
The pixel allowance above is an example value, not a recommended universal threshold. Start with strict comparison settings, inspect failures, and adjust only when you understand the source of the variation. Official option behavior and current defaults are documented in Playwright’s visual comparisons guide.
7. Cover viewport and browser differences deliberately
RTL layout bugs may appear only at a particular width or in an interaction state. Add coverage for the viewports and browsers that matter to your users. Keep the baseline environment consistent within each comparison set; separate projects or snapshot directories can be useful when browser or platform rendering is intentionally different.
A focused mobile scenario might look like this:
test('Arabic landing page on a narrow viewport', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/?locale=ar');
await expect(page.getByRole('main')).toBeVisible();
await expect(page).toHaveScreenshot('landing-ar-mobile.png', {
fullPage: true,
});
});
For a broader browser matrix, add Playwright projects for the engines and device profiles the application supports. A baseline for one browser is not automatically a valid baseline for another because rendering can differ.
8. Update snapshots after intentional changes
When a reviewed product change intentionally alters appearance, update the reference screenshots with:
npx playwright test --update-snapshots
Review every changed baseline in version control. Updating snapshots records the new output; it does not establish that the new RTL layout is correct. Include the diff in code review and verify the affected locale, state, and viewport.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Missing expected snapshot on the first run | No baseline exists yet for this test and environment. | Inspect the actual capture, then create and commit the reviewed baseline. |
| Snapshot differs on every run | Dynamic content, unfinished loading, animation, hover state, or inconsistent host/browser conditions. | Wait for app-specific readiness, stabilize data and pointer position, and keep the comparison environment consistent. |
| Page looks left-to-right in the capture | The test may have navigated to the default locale or captured before the app applied RTL state. | Use the real locale setup and wait for a reliable signal that localization is ready before taking the screenshot. |
| Only CI reports visual diffs | CI may use a different OS, browser build, font availability, or headless configuration from local runs. | Generate and compare baselines in a consistent CI image and pinned Playwright browser version. |
| Large full-page image changes unexpectedly | Content height may vary, lazy content may load at different times, or the page may contain dynamic sections. | Stabilize the page data and readiness condition. Capture the viewport if full-page appearance is not required. |
| Repeated snapshot update hides a regression | Baselines were refreshed without reviewing the diff. | Restore the prior reference, inspect expected/actual/diff, then update only for a verified intentional change. |
| Screenshot passes but RTL behavior is still wrong | The baseline itself may encode the wrong locale or an incorrect layout. | Add explicit locale and behavior assertions, and review the baseline against the product requirement. |
10. Run and interpret the test
Run the suite with:
npx playwright test
Run a single file or test by passing a path or title filter, for example npx playwright test tests/rtl.spec.ts. When a comparison fails, use Playwright’s reported artifacts to inspect the expected, actual, and diff images. Treat the screenshot as evidence about appearance in that controlled run, not as proof of correctness across every browser, locale, or user state.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. It can capture a URL as PNG, JPEG, WebP, or PDF; this is useful for a quick rendered-page capture, while Playwright remains the right fit for automated assertions against committed test baselines.
For an RTL route that accepts its locale in the URL, make one request (replace the example target with your actual URL):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/?locale=ar -o shot.webp
See the ScreenshotNeo API documentation for request options and configuration.
- Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, with page verdict and billing response headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients. - The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does a screenshot test verify that my page uses the correct RTL locale?
No. It compares rendered output with a reference. Set the intended locale explicitly and add assertions for the application state that matters.
Should every supported locale have a separate baseline?
If the locales produce meaningfully different visual output, give each scenario a distinct baseline so a change in one does not overwrite another’s expected appearance.
Can an ARIA snapshot replace a visual screenshot?
No. An ARIA snapshot checks accessible structure; a screenshot checks pixels. Use both when the requirement includes both kinds of behavior.
Can I use a remote screenshot API for a Playwright visual regression suite?
A remote capture can produce an image for inspection, but this workflow uses Playwright Test’s screenshot assertion and stored baselines. Keep the capture and comparison environment consistent when relying on pixel diffs.


