Static accessibility linting vs runtime testing

Static linting and runtime testing answer different questions. ZemDomu checks supported source patterns before rendering; axe evaluates supported rules against a rendered state; browser, manual, and assistive-technology tests add behavior and human judgment. No single clean result establishes that an application is accessible.

Match each layer to the evidence it provides

ZemDomu is useful for early feedback on supported headings, landmarks, accessible-name sources, static ARIA values, ids, and composed source structure. It cannot prove final runtime values, contrast, focus movement, announcements, or interaction success.

Axe evaluates automatically detectable rules in the rendered state that is scanned. Hidden, closed, or otherwise unvisited states need to be exposed and scanned separately, and a clean result does not prove WCAG conformance.

Browser tests exercise only the states, inputs, and assertions you write. Manual review adds judgment about meaning, visible focus, zoom, reflow, instructions, and task coherence. Assistive-technology testing provides evidence for named browser, operating-system, and AT combinations rather than universal interoperability.

Follow one dialog issue through every layer

ZemDomu can report the icon-only close button because it has no supported source-level accessible name. Adding aria-label addresses that static defect, but it does not prove that the dialog receives focus, closes with Escape, restores focus, or is announced correctly.

// Broken: the close button has no accessible name.
<button type="button" onClick={() => setOpen(false)}>
  <CloseIcon aria-hidden="true" />
</button>

// Source correction: name the action.
<button
  type="button"
  aria-label="Close filter dialog"
  onClick={() => setOpen(false)}
>
  <CloseIcon aria-hidden="true" />
</button>

Run axe in the state users reach

Open the dialog before scanning it. Phrase the result as no automatically detectable violations in this tested state, not as proof that the dialog is accessible. Add separate browser assertions for intended initial focus, keyboard operation, Escape behavior, and focus return.

import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";

test("filter dialog automated checks", async ({ page }) => {
  await page.goto("/reports");
  await page.getByRole("button", { name: "Filters" }).click();

  const dialog = page.getByRole("dialog", { name: "Filter results" });
  await expect(dialog).toBeVisible();

  const results = await new AxeBuilder({ page })
    .include('[role="dialog"]')
    .analyze();

  expect(results.violations).toEqual([]);
});

Use a layered workflow

This sequence is a practical inference from the documented boundaries of each layer. Adjust the browsers, assistive technologies, states, and manual scope to the product's audience and risk.

  1. Run ZemDomu while editing and before commit for supported source defects.
  2. Render every important route and component state in browser tests.
  3. Run axe after interactions expose dialogs, menus, validation errors, and other conditional content.
  4. Add explicit keyboard and focus assertions for critical tasks.
  5. Manually review meaning, visible behavior, zoom, reflow, and flows that require judgment.
  6. Test representative browser and assistive-technology combinations and document the exact scope.

State precisely what passed

Say that ZemDomu reported no findings for its enabled rules in the scanned files, axe reported no violations in the rendered states exercised, or named keyboard and focus assertions passed in the tested browsers.

For manual and assistive-technology reviews, name the routes, tasks, viewport settings, browsers, and AT combinations. Avoid claims such as fully accessible, catches all issues, or proves WCAG conformance, and avoid unsupported defect-detection percentages.

Related ZemDomu rules

Sources and further reading