Skip to content
Get a free audit

Currency

Project Spark 8 min read

An Accessible, RTL-Ready Admin UI (WCAG 2.2 AA)

WCAG 2.2 AA in a WordPress admin plugin, contrast fixes, focus traps, ARIA live regions, and CSS logical properties that make RTL layouts free.

Bernard

An SEO dashboard used daily by a Hebrew-speaking team has two non-negotiables most plugins skip: genuine accessibility and genuine right-to-left support. Project Spark’s admin went through a measured accessibility campaign, from 154 automated audit errors down to a handful inherited from WordPress core, and an RTL overhaul that ultimately deleted most of its RTL-specific code. Both journeys have transferable lessons.

Key takeaway: Accessibility is measurable, not aspirational, run automated audits, compute real contrast ratios, and pin every fix with a test. And modern RTL support is mostly deletion: CSS logical properties make one stylesheet serve both directions.

Why an admin interface deserves this investment

A common objection deserves answering first: this is a back-office dashboard, not a public website, why hold it to WCAG 2.2 AA? Three reasons, each commercial rather than idealistic.

First, the client’s team spends hours per week inside these 26 dashboard screens and the 16-step setup wizard. Every unlabelled field, every low-contrast status colour, every modal that swallows keyboard focus is a small recurring tax on real working time, multiplied across every session, forever. Second, accessibility requirements do not check whether a user is an employee: a team member who relies on a screen reader, keyboard navigation, or high contrast has exactly the same needs in the admin as on any public page, and hiring should never be constrained by tooling. Third, the engineering reason, an interface built to be programmatically legible (names on every control, status changes announced, structure expressed semantically) is also an interface that automated tests can verify. Accessibility and testability are the same property viewed from two angles.

The accessibility campaign, by the numbers

Automated auditing (pa11y) started at 154 errors across the dashboard. The number is the point: accessibility work went from a vague aspiration to a burn-down chart. The fixes, in rough order of impact:

  • Colour contrast, computed, not judged by eye. A script measured every text/background token combination against WCAG’s 4.5:1 threshold. Six failing combinations, including status colours borrowed from WordPress core that fail on light backgrounds, were replaced with darker variants (5.1–6.7:1). The lesson: platform default palettes are not automatically compliant. The bright green and red that WordPress uses for success and error states are recognizable, but at small text sizes on light backgrounds they measure below the threshold; the darker variants keep the semantic colour while passing with room to spare.
  • Labels for every control. Dozens of inputs, selects, and textareas gained programmatic names, visible labels where design allowed, aria-label/aria-labelledby where headings already served as visual labels. A structural test now scans every form control in every admin template and every JavaScript-rendered template string and fails CI if any control lacks an accessible name. That second half matters: interfaces increasingly render controls from JavaScript, and an audit that only reads server templates misses them entirely.
  • Status that speaks. Colour-dot indicators became role="img" with text alternatives (colour alone is WCAG-illegal signalling); long-running AI operations write into aria-live log regions so screen-reader users hear progress as entries are appended; every busy state toggles aria-busy. Before this work, a screen-reader user who started a bulk AI operation experienced pure silence until it finished, or didn’t.
  • A real focus trap. Modal dialogs got Tab-cycling containment, Escape handling, and focus restoration. Embarrassing discovery: an earlier “focus trap” existed only inside a test file, the tests passed while production had no trap at all. It’s now a shared production utility, and the same tests pin the real thing.
  • Structural semantics: landmark regions, skip links, heading hierarchy without skipped levels, scope attributes on ~90 table headers, 30-pixel minimum touch targets on icon buttons. Single-character remove buttons had been rendering at content width, around 16 pixels, which is a genuinely difficult touch target; the minimum square hit area fixed a frustration that sighted mouse users had also been feeling without naming it.

The test that tested itself

The focus-trap discovery deserves its own section because it names a failure mode most teams have and few notice. The trap had been written test-first: a well-intentioned developer wrote the containment logic and thorough tests for it, Tab cycling, Escape behaviour, focus restoration, ten scenarios, all green. But the implementation lived inside the test file as a fixture, and the production modals never imported it. Every build reported a fully tested focus trap; no user had ever been protected by it.

The generalizable defect: a test that supplies its own implementation verifies nothing about the product. The fix was to promote the trap into a shared production utility, make the real modals use it, and point the existing tests at the production code, same assertions, finally aimed at reality. The audit habit it created: for every tested behaviour, confirm the test imports the code users actually run. It sounds too obvious to state. It survived unnoticed in a codebase with thousands of tests.

RTL: the best code is deleted code

The original RTL support was a pile of override rules, flip this margin, mirror that padding, maintained in parallel with the left-to-right styles. Every layout change had to be made twice, and each forgotten mirror produced a subtle Hebrew-side misalignment that only a Hebrew-reading tester would report. This is the standard RTL tax, and most products pay it forever.

The overhaul replaced physical CSS properties (margin-left, padding-right, text-align: left) with logical properties (margin-inline-start, padding-inline-end, text-align: start) across the entire stylesheet. A logical property names the role of an edge, where text begins, rather than its compass direction, so the browser resolves it correctly in either direction. Fifty-plus directional rules became zero; ~80 lines of RTL overrides were deleted; Hebrew layout now falls out of the same stylesheet automatically, driven by WordPress core’s dir="rtl".

The deletion is the headline, but the durable win is the default it creates: every future interface addition ships correct in Hebrew with no extra work, because there is no second stylesheet to forget. RTL correctness stopped being a task and became a property of the system. A handful of genuinely directional rules remain, a sidebar that should sit on the reading-start side, an icon that implies direction, and their rarity is what makes them reviewable.

Design tokens completed the picture: one :root block of colour and spacing variables, with a dark-mode media query overriding tokens, flipping the entire palette, including the contrast-verified status colours, in a dozen lines. Contrast was then re-verified against the dark palette separately, because a combination that passes on white can fail on near-black.

Keeping it fixed: accessibility as regression tests

The campaign’s lasting output is not the fixed interface, it is the set of automated gates that keep it fixed. Form-control naming is enforced by a structural test across all templates and JavaScript-rendered markup. Image elements are scanned for text alternatives. The contrast-measuring script can re-run against any palette change. The focus trap’s tests now exercise production code. Heading hierarchy and table-header scope are pinned. In a project whose automated suite grew past 4,700 checks, accessibility assertions sit alongside security and data-integrity ones, and a regression in any of them fails the build before a user encounters it. Accessibility that lives in a one-time audit decays; accessibility that lives in CI compounds.

What automated audits cannot catch

The 154-error burn-down is a satisfying chart, but automated auditing has a known ceiling, and pretending otherwise would misrepresent the work. Tools like pa11y verify what is mechanically checkable: missing labels, contrast values, absent attributes, structural violations. They cannot verify whether the experience works, whether the tab order matches the visual order, whether a screen reader announces a bulk operation’s progress in a way a human can follow, whether the focus lands somewhere sensible after a dialog closes, whether an error message actually helps.

Those questions were answered the manual way. Keyboard-only passes through every major workflow: the 16-step wizard, the review-and-approve AI flows, the bulk operations, no mouse allowed, every action reachable, every focus state visible. Screen-reader passes over the status-heavy screens, listening for what a non-sighted operator would actually hear during a long-running AI job (before the aria-live work: silence; after: a narrated log of progress). And because the client’s team works in Hebrew, the manual passes were run in the right-to-left layout too, which is how you notice the places where a physically-positioned element made sense in one direction and not the other.

The honest division of labour: automation catches regressions and enforces the floor; humans verify the experience and set the bar. The automated suite exists so that the expensive manual verification only has to be done once per workflow, not once per release, the machines guard what the humans have already judged.

A starting checklist for your own admin screens

  1. Run an automated audit (pa11y, axe, or similar) and record the number, you need a baseline to burn down.
  2. Compute contrast for every text/background pair in your palette, light and dark modes separately. Do not trust platform defaults.
  3. Name every control, and audit JavaScript-rendered markup, not just server templates.
  4. Announce async status with aria-live regions and aria-busy; never signal with colour alone.
  5. Verify your modals trap Tab, handle Escape, and restore focus, and that the tested code is the shipped code.
  6. Migrate to logical properties and delete your RTL override file; keep only the genuinely directional exceptions.
  7. Turn each fix into a test so the next redesign cannot silently undo it.

Why bother, commercially

The client’s team lives in this dashboard for hours weekly. Accessibility work is usability work: clearer labels, visible focus, readable contrast, and announced progress benefit every user, and the RTL-via-logical-properties approach means future UI ships correct in Hebrew by default, not as a follow-up ticket. The 154-to-single-digits burn-down took a focused campaign measured in days, not months; the deleted override stylesheet repays that effort on every future release.

Next: Part 18, Internal Linking Without Breaking Page Builders

FAQ

How do I make a WordPress plugin accessible?

Audit with automated tools, compute contrast ratios against WCAG thresholds, give every form control a programmatic name, announce async status via aria-live, manage focus in modals, and enforce each fix with automated tests.

What are CSS logical properties?

Direction-relative properties (margin-inline-start instead of margin-left) that automatically mirror in right-to-left contexts, letting one stylesheet serve LTR and RTL without override rules.

What contrast ratio does WCAG 2.2 AA require?

4.5:1 for normal text and 3:1 for large text and UI components, verify by computation, not by eye, and re-verify dark-mode palettes separately.

Do WordPress core admin styles meet WCAG?

Not uniformly, several core status colors fail 4.5:1 on light backgrounds. Plugins inheriting core palettes should measure and darken where needed.

Does an admin dashboard need to meet WCAG?

Yes, accessibility needs do not stop at the login screen. Team members using screen readers, keyboards, or high contrast have identical requirements in back-office tools, and many jurisdictions treat workplace software accordingly.

How do I add right-to-left support to an existing interface?

Replace physical CSS properties with logical ones (margin-inline-start, padding-inline-end, text-align: start), delete the override stylesheet, and keep only genuinely directional rules. Most RTL support is deletion, not addition.

Keep reading in this series

Keep reading

An Accessible, RTL-Ready Admin UI (WCAG 2.2 AA)

WCAG 2.2 AA in a WordPress admin plugin — contrast fixes, focus traps, ARIA live regions, and CSS logical properties that make RTL layouts free.

Bernard

Building a Hebrew-First SEO Plugin (“Project Spark”) — Part 17 of 20

An SEO dashboard used daily by a Hebrew-speaking team has two non-negotiables most plugins skip: genuine accessibility and genuine right-to-left support. Project Spark’s admin went through a measured accessibility campaign — from 154 automated audit errors down to a handful inherited from WordPress core — and an RTL overhaul that ultimately deleted most of its RTL-specific code. Both journeys have transferable lessons.

Key takeaway: Accessibility is measurable, not aspirational — run automated audits, compute real contrast ratios, and pin every fix with a test. And modern RTL support is mostly deletion: CSS logical properties make one stylesheet serve both directions.

Why an admin interface deserves this investment

A common objection deserves answering first: this is a back-office dashboard, not a public website — why hold it to WCAG 2.2 AA? Three reasons, each commercial rather than idealistic.

First, the client’s team spends hours per week inside these 26 dashboard screens and the 16-step setup wizard. Every unlabelled field, every low-contrast status colour, every modal that swallows keyboard focus is a small recurring tax on real working time — multiplied across every session, forever. Second, accessibility requirements do not check whether a user is an employee: a team member who relies on a screen reader, keyboard navigation, or high contrast has exactly the same needs in the admin as on any public page, and hiring should never be constrained by tooling. Third — the engineering reason — an interface built to be programmatically legible (names on every control, status changes announced, structure expressed semantically) is also an interface that automated tests can verify. Accessibility and testability are the same property viewed from two angles.

The accessibility campaign, by the numbers

Automated auditing (pa11y) started at 154 errors across the dashboard. The number is the point: accessibility work went from a vague aspiration to a burn-down chart. The fixes, in rough order of impact:

  • Colour contrast, computed, not judged by eye. A script measured every text/background token combination against WCAG’s 4.5:1 threshold. Six failing combinations — including status colours borrowed from WordPress core that fail on light backgrounds — were replaced with darker variants (5.1–6.7:1). The lesson: platform default palettes are not automatically compliant. The bright green and red that WordPress uses for success and error states are recognizable, but at small text sizes on light backgrounds they measure below the threshold; the darker variants keep the semantic colour while passing with room to spare.
  • Labels for every control. Dozens of inputs, selects, and textareas gained programmatic names — visible labels where design allowed, aria-label/aria-labelledby where headings already served as visual labels. A structural test now scans every form control in every admin template and every JavaScript-rendered template string and fails CI if any control lacks an accessible name. That second half matters: interfaces increasingly render controls from JavaScript, and an audit that only reads server templates misses them entirely.
  • Status that speaks. Colour-dot indicators became role="img" with text alternatives (colour alone is WCAG-illegal signalling); long-running AI operations write into aria-live log regions so screen-reader users hear progress as entries are appended; every busy state toggles aria-busy. Before this work, a screen-reader user who started a bulk AI operation experienced pure silence until it finished — or didn’t.
  • A real focus trap. Modal dialogs got Tab-cycling containment, Escape handling, and focus restoration. Embarrassing discovery: an earlier “focus trap” existed only inside a test file — the tests passed while production had no trap at all. It’s now a shared production utility, and the same tests pin the real thing.
  • Structural semantics: landmark regions, skip links, heading hierarchy without skipped levels, scope attributes on ~90 table headers, 30-pixel minimum touch targets on icon buttons. Single-character remove buttons had been rendering at content width — around 16 pixels — which is a genuinely difficult touch target; the minimum square hit area fixed a frustration that sighted mouse users had also been feeling without naming it.

The test that tested itself

The focus-trap discovery deserves its own section because it names a failure mode most teams have and few notice. The trap had been written test-first: a well-intentioned developer wrote the containment logic and thorough tests for it — Tab cycling, Escape behaviour, focus restoration, ten scenarios, all green. But the implementation lived inside the test file as a fixture, and the production modals never imported it. Every build reported a fully tested focus trap; no user had ever been protected by it.

The generalizable defect: a test that supplies its own implementation verifies nothing about the product. The fix was to promote the trap into a shared production utility, make the real modals use it, and point the existing tests at the production code — same assertions, finally aimed at reality. The audit habit it created: for every tested behaviour, confirm the test imports the code users actually run. It sounds too obvious to state. It survived unnoticed in a codebase with thousands of tests.

RTL: the best code is deleted code

The original RTL support was a pile of override rules — flip this margin, mirror that padding — maintained in parallel with the left-to-right styles. Every layout change had to be made twice, and each forgotten mirror produced a subtle Hebrew-side misalignment that only a Hebrew-reading tester would report. This is the standard RTL tax, and most products pay it forever.

The overhaul replaced physical CSS properties (margin-left, padding-right, text-align: left) with logical properties (margin-inline-start, padding-inline-end, text-align: start) across the entire stylesheet. A logical property names the role of an edge — where text begins — rather than its compass direction, so the browser resolves it correctly in either direction. Fifty-plus directional rules became zero; ~80 lines of RTL overrides were deleted; Hebrew layout now falls out of the same stylesheet automatically, driven by WordPress core’s dir="rtl".

The deletion is the headline, but the durable win is the default it creates: every future interface addition ships correct in Hebrew with no extra work, because there is no second stylesheet to forget. RTL correctness stopped being a task and became a property of the system. A handful of genuinely directional rules remain — a sidebar that should sit on the reading-start side, an icon that implies direction — and their rarity is what makes them reviewable.

Design tokens completed the picture: one :root block of colour and spacing variables, with a dark-mode media query overriding tokens — flipping the entire palette, including the contrast-verified status colours, in a dozen lines. Contrast was then re-verified against the dark palette separately, because a combination that passes on white can fail on near-black.

Keeping it fixed: accessibility as regression tests

The campaign’s lasting output is not the fixed interface — it is the set of automated gates that keep it fixed. Form-control naming is enforced by a structural test across all templates and JavaScript-rendered markup. Image elements are scanned for text alternatives. The contrast-measuring script can re-run against any palette change. The focus trap’s tests now exercise production code. Heading hierarchy and table-header scope are pinned. In a project whose automated suite grew past 4,700 checks, accessibility assertions sit alongside security and data-integrity ones, and a regression in any of them fails the build before a user encounters it. Accessibility that lives in a one-time audit decays; accessibility that lives in CI compounds.

What automated audits cannot catch

The 154-error burn-down is a satisfying chart, but automated auditing has a known ceiling, and pretending otherwise would misrepresent the work. Tools like pa11y verify what is mechanically checkable: missing labels, contrast values, absent attributes, structural violations. They cannot verify whether the experience works — whether the tab order matches the visual order, whether a screen reader announces a bulk operation’s progress in a way a human can follow, whether the focus lands somewhere sensible after a dialog closes, whether an error message actually helps.

Those questions were answered the manual way. Keyboard-only passes through every major workflow: the 16-step wizard, the review-and-approve AI flows, the bulk operations — no mouse allowed, every action reachable, every focus state visible. Screen-reader passes over the status-heavy screens, listening for what a non-sighted operator would actually hear during a long-running AI job (before the aria-live work: silence; after: a narrated log of progress). And because the client’s team works in Hebrew, the manual passes were run in the right-to-left layout too — which is how you notice the places where a physically-positioned element made sense in one direction and not the other.

The honest division of labour: automation catches regressions and enforces the floor; humans verify the experience and set the bar. The automated suite exists so that the expensive manual verification only has to be done once per workflow, not once per release — the machines guard what the humans have already judged.

A starting checklist for your own admin screens

  1. Run an automated audit (pa11y, axe, or similar) and record the number — you need a baseline to burn down.
  2. Compute contrast for every text/background pair in your palette, light and dark modes separately. Do not trust platform defaults.
  3. Name every control — and audit JavaScript-rendered markup, not just server templates.
  4. Announce async status with aria-live regions and aria-busy; never signal with colour alone.
  5. Verify your modals trap Tab, handle Escape, and restore focus — and that the tested code is the shipped code.
  6. Migrate to logical properties and delete your RTL override file; keep only the genuinely directional exceptions.
  7. Turn each fix into a test so the next redesign cannot silently undo it.

Why bother, commercially

The client’s team lives in this dashboard for hours weekly. Accessibility work is usability work: clearer labels, visible focus, readable contrast, and announced progress benefit every user — and the RTL-via-logical-properties approach means future UI ships correct in Hebrew by default, not as a follow-up ticket. The 154-to-single-digits burn-down took a focused campaign measured in days, not months; the deleted override stylesheet repays that effort on every future release.

FAQ

How do I make a WordPress plugin accessible?

Audit with automated tools, compute contrast ratios against WCAG thresholds, give every form control a programmatic name, announce async status via aria-live, manage focus in modals — and enforce each fix with automated tests.

What are CSS logical properties?

Direction-relative properties (margin-inline-start instead of margin-left) that automatically mirror in right-to-left contexts, letting one stylesheet serve LTR and RTL without override rules.

What contrast ratio does WCAG 2.2 AA require?

4.5:1 for normal text and 3:1 for large text and UI components — verify by computation, not by eye, and re-verify dark-mode palettes separately.

Do WordPress core admin styles meet WCAG?

Not uniformly — several core status colors fail 4.5:1 on light backgrounds. Plugins inheriting core palettes should measure and darken where needed.

Does an admin dashboard need to meet WCAG?

Yes — accessibility needs do not stop at the login screen. Team members using screen readers, keyboards, or high contrast have identical requirements in back-office tools, and many jurisdictions treat workplace software accordingly.

How do I add right-to-left support to an existing interface?

Replace physical CSS properties with logical ones (margin-inline-start, padding-inline-end, text-align: start), delete the override stylesheet, and keep only genuinely directional rules. Most RTL support is deletion, not addition.


Previous: Part 16: WordPress SEO Performance at 10,000 Posts
Next: Part 18: Automated Internal Linking Without Breaking Sites

Keep reading

Newsletter

Get the next playbook in your inbox.

One short, no-fluff email per fortnight.

Ready when you are

Let's map the next 90 days of growth.

Book a no-pressure call. We'll review your funnel, share quick wins, and outline what compounding growth could look like for your business.

Get a free audit

A note on cookies

We use cookies to measure how this site performs and to make it better. Analytics cookies only run if you accept.

Read our privacy policy