skills/angular-accessibility/SKILL.md
Audit and fix common accessibility issues in Angular templates and Angular Material components. Use when the user mentions Lighthouse, axe, screen readers, keyboard navigation, ARIA, asks to fix a11y issues in Angular HTML templates, or after any feature, bug fix, or refactor that changed Angular templates.
npx skillsauth add fmflurry/settings-opencode angular-accessibilityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Run this skill as a final verification step whenever the implementation modified .html templates or materially changed rendered UI structure, even if the original task did not mention accessibility.
Typical triggers:
<button>, <a>, <label>, <main>, <table>, and <th> before ARIA workarounds<label for>, <mat-label>, or aria-labelledby before aria-label[attr.aria-*][aria-label] or [aria-labelledby] without attr.role="button" on <button>, role="checkbox" on <input type="checkbox">, etc.)npx eslint "src/app/<feature>/**/*.html"
Tab / Shift+Tab reaches all interactive controls in a logical orderEnter / Space activates buttons, toggles, and checkboxesEscape closes dialogs, menus, or drawers where applicableAny <button> whose only content is a <mat-icon> or similar icon has no accessible name.
Fix: add a translated accessible name and mark the icon as decorative.
<!-- ❌ Before -->
<button type="button" (click)="onClose()">
<mat-icon>close</mat-icon>
</button>
<!-- ✅ After -->
<button
type="button"
[attr.aria-label]="t('scope.key.close-btn-aria')"
(click)="onClose()"
>
<mat-icon aria-hidden="true">close</mat-icon>
</button>
Same pattern applies to pagination, toggle, toolbar, and other icon-only actions.
Prefer a visible label first. Use aria-label only when a visible label or aria-labelledby is not practical.
Standard forms: use <label for> or <mat-label>.
<!-- ✅ Preferred in a regular form -->
<mat-form-field>
<mat-label>{{ t('scope.key.amount') }}</mat-label>
<input matInput id="amount" type="number" />
</mat-form-field>
Dense table/grid cells: use row-level context so each control is uniquely identifiable.
<!-- ❌ Before -->
<input matInput type="number" [value]="row.amount" />
<mat-select [value]="row.type"></mat-select>
<!-- ✅ After -->
<input
matInput
type="number"
[value]="row.amount"
[attr.aria-label]="t('scope.key.amount-aria', { invoiceNumber: row.invoiceNumber })"
/>
<mat-select
[value]="row.type"
[aria-label]="t('scope.key.type-aria', { invoiceNumber: row.invoiceNumber })"
>
</mat-select>
Empty mat-checkbox components with no projected text need an accessible name.
<!-- ❌ Before -->
<mat-checkbox
[checked]="allSelected()"
(change)="onToggleAll($event.checked)"
/>
<!-- ✅ After - header (select-all) -->
<mat-checkbox
[aria-label]="t('scope.key.select-all-aria')"
[checked]="allSelected()"
(change)="onToggleAll($event.checked)"
/>
<!-- ✅ After - row (select one) -->
<mat-checkbox
[aria-label]="t('scope.key.select-row-aria', { invoiceNumber: row.invoiceNumber })"
[checked]="row.selected"
(change)="onToggleItem(row.id, $event.checked)"
/>
Clickable <div> and <span> elements break keyboard and assistive technology behavior.
Fix: replace them with native interactive elements whenever possible.
<!-- ❌ Before -->
<div class="close-action" (click)="onClose()">
<mat-icon>close</mat-icon>
</div>
<!-- ✅ After -->
<button
type="button"
class="close-action"
[attr.aria-label]="t('scope.key.close-btn-aria')"
(click)="onClose()"
>
<mat-icon aria-hidden="true">close</mat-icon>
</button>
Use <a> for navigation and <button> for in-page actions.
maximum-scale or user-scalable=no in the viewport meta tag prevents low-vision users from magnifying the page.
Fix (src/index.html): keep only width=device-width, initial-scale=1.0.
<!-- ❌ Before -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<!-- ✅ After -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<main> landmarkScreen readers use landmarks to jump between page regions. A page must have exactly one <main> element.
In this project the layout can render two conditional branches (mobile / desktop). Each branch should expose its content wrapper as <main>. Since they are mutually exclusive, only one <main> is in the DOM at a time.
<!-- mobile: main.component.html -->
<!-- ❌ Before: <div class="main-container"> -->
<main class="main-container" cdkVirtualScrollingElement>
<gc-sub-header />
<router-outlet />
</main>
<!-- desktop: page.component.html -->
<!-- ❌ Before: <div class="desktop-page-content"> -->
<main class="desktop-page-content">
<router-outlet />
</main>
SCSS that targets
.main-containeror.desktop-page-contentkeeps working when only the element tag changes.
mat-progress-spinner already follows the ARIA progressbar pattern. Do not replace it with role="status".
Fix:
aria-live="polite" message only when the loading state itself must be announced<!-- ✅ Spinner keeps its default progressbar semantics -->
<mat-progress-spinner
[attr.aria-label]="t('shared.loading-aria')"
mode="indeterminate"
diameter="32"
/>
<!-- ✅ Optional live region when the loading state should be announced -->
@if (isLoading()) {
<span aria-live="polite">{{ t('shared.loading-aria') }}</span>
}
Interactive overlays and expandable sections must expose state and preserve focus behavior.
Verify:
aria-expanded when they show or hide related content<button
type="button"
aria-controls="filters-panel"
[attr.aria-expanded]="isFiltersOpen()"
(click)="toggleFilters()"
>
{{ t('scope.key.filters') }}
</button>
@if (isFiltersOpen()) {
<section id="filters-panel">...</section>
}
For Angular Material dialogs, keep the built-in focus management unless there is a concrete need to change it.
All accessible names and descriptions must use translated strings via Transloco. Never hard-code French or any other language directly in templates.
t('scope.key') pattern-aria to accessible-name keys and -description when the text is used for extra contextfr-FR.jsonExample JSON additions:
{
"table": {
"select-all-aria": "Sélectionner toutes les échéances",
"select-row-aria": "Sélectionner l'échéance {{invoiceNumber}}",
"discount-amount-aria": "Montant escompte pour la facture {{invoiceNumber}}"
},
"pagination": {
"previous-aria": "Page précédente",
"next-aria": "Page suivante"
},
"selection-detail": {
"close-btn-aria": "Fermer le volet de détails"
}
}
After fixing, verify:
maximum-scale or user-scalable=no<main> landmarkaria-hidden="true"aria-labelledby, or aria-label as appropriatemat-checkbox components have [aria-label] or [aria-labelledby]div and span elements were replaced with semantic controlsaria-expanded when applicablealt; decorative images use empty alt=""npx eslint "src/app/<feature>/**/*.html"development
Scaffolds and extends Angular 18+ standalone features using Clean Architecture with DDD layering (presentation/application/domain/infrastructure), custom signal-based stores, facade pattern, and ports/adapters dependency inversion. Use when creating new Angular features/domains, adding use cases/facades/stores/ports/adapters, refactoring legacy NgModule/NgRx code toward clean architecture, or working with cross-domain communication via context registry.
development
Pre-merge code review for Angular + TypeScript pull requests. Diffs current branch against a target branch, applies Angular-specific checklists (signals, RxJS, clean architecture, flurryx, TS strict), runs lint + tsc, and emits a tiered report (verbose for juniors, terse for seniors). Auto-loads project AGENTS.md rules. Use when user runs /cop-review, says "pre-merge review", "review before merging", "check my PR against <branch>", or invokes the merge-cop agent.
testing
Use this skill for any git work such as creating branches, staging changes, writing commit messages, pushing branches, or preparing pull requests. Delegates git execution to the git-specialist agent.
development
Signal-first reactive state management for Angular. Bridge RxJS streams into cache-aware stores, keyed resources, mirrored state, and replayable history. Use when generating or modifying Angular code that uses flurryx for state management, or when scaffolding new feature modules that follow the flurryx facade pattern.