accessibility-as-code
Skill32 starsChanged 4 months agoMITView on GitHub
Enforces accessibility as a correctness property authored into each widget — Semantics(button/label) or ExcludeSemantics on every node, a11y state read from MediaQuery not app state, never MediaQuery.withClampedTextScaling / textScaleFactor / FittedBox / TextOverflow.ellipsis to fit a label, non-color redundant channels (icon+label+shape+text) for every state, contrast against composited backgrounds (4.5:1 body / 3:1 large), 44px single-tap targets, OrdinalSortKey traversal, and honoring boldText / reduce-motion. Use when adding a GestureDetector/InkWell or any tap target, adding an Icon or Image, reaching for withClampedTextScaling/FittedBox/ellipsis/textScaleFactor to make text fit, encoding state via color, sizing type, ordering focus traversal, or reviewing any View for screen-reader/switch/low-vision support.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- zakariaf/CatchLawword for word, first seen Aug 1, 2026
Other versions1 project uses a version of this.
Word for word · 1
- zakariaf/CatchLaw★ 0100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository40 other files it gives its agents.
- AGENTS.mdAGENTS.md
- adaptive-layoutSkill
- ads-and-iap-monetizationSkill
- app-startup-and-bootstrapSkill
- async-safetySkill
- ci-pipeline-and-gatesSkill
- codegen-and-toolchainSkill
- custom-canvas-and-gesturesSkill
- dart3-idioms-and-coding-standardsSkill
- dartdoc-conventionsSkill
- data-export-and-restoreSkill
- dependency-hygieneSkill
- design-review-workflowSkill
- design-system-structureSkill
- error-handling-typed-resultsSkill
- flutter-architectureSkill
- flutter-conventions-indexSkill
- flutter-performanceSkill
- forms-and-inputSkill
- i18n-rtl-l10nSkill
- lint-and-style-configSkill
- local-notifications-schedulerSkill
- motion-and-hapticsSkill
- naming-conventionsSkill
- navigation-and-routingSkill
- persistence-driftSkill
- project-structure-and-packagesSkill
- release-and-store-shippingSkill
- run-codegenSkill
- run-goldens-rebaselineSkill
- run-migrationSkill
- scaffold-feature-moduleSkill
- seeded-determinism-and-golden-vectorsSkill
- service-boundary-and-nativeSkill
- state-management-riverpodSkill
- testing-strategySkill
- ui-states-and-feedbackSkill
- value-objects-money-and-unitsSkill
- widget-compositionSkill
- widget-golden-and-a11y-testingSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 4 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Read platform accessibility state from context, app state from your notifier:
- Very specific: line 131
Use FontFeature.tabularFigures() (or .monospacedDigit()-style tabular numerals) wherever numbers change or align so they don't reflow or …
- Distinctive: line 157
Verify contrast against what the pixel actually composites to, in both light and dark, not against the nominal token:
- Distinctive: line 160
Never render a critical number or label on a gradient, translucent glass, or chip fill.
- Distinctive: line 162
Avoid the red/green trap: a neutral value is neutral ink, "success" is green + ✓ glyph + the word, danger-red appears only on destructive …
- Distinctive: line 163
Verify the whole UI in a grayscale pass and deuteranopia / protanopia / tritanopia simulation.
- Very specific: line 194
Every Icon/Image labelled or explicitly ExcludeSemantics.
- Very specific: line 195
No clamp, no FittedBox, no ellipsis on a real label.
- Very specific: line 197
Every state carries a non-color channel.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Accessibility is a coding standard, not a checklist
Accessibility is authored into the widget as you write it — the same tier as "the button actually works." There is no lint for any of this: flutter_lints and very_good_analysis ship zero a11y rules. Enforcement is the widget itself, review of your own diff, and widget tests. Applies to every interactive View, every Icon/Image, every state, and every color choice.
Testing mechanics (guideline matchers, golden lanes, RTL, honest conformance limits) live in widget-golden-and-a11y-testing; this skill is the authoring discipline.
Non-negotiable rules
- Every interactive node gets
Semantics(button: true, label: ...)— a rawGestureDetectorwith no semantics silently locks out every screen-reader and switch user. Correct semantics make TalkBack, VoiceOver, Switch Access and Switch Control work for free; it is definition-of-done, never a backlog ticket. - Every
Icon/Imagegets asemanticLabel, or is wrapped inExcludeSemanticsbecause it is decorative. There is no third option — an unlabelledIconis invisible to a screen reader. - Read a11y state from
MediaQueryat build time, never from app state.MediaQueryis anInheritedWidgetwith correct-by-construction invalidation; pushing it through a provider trades a compiler-guaranteed rebuild for a frame of staleness in the one area where being wrong is total failure. - Never
MediaQuery.withClampedTextScaling, nevertextScaleFactor. Both are clamping hacks that silently defeat the text-scale matrix while contrast and tap-target guidelines still pass green. Enforce with a source grep (see Anti-patterns). - Never
FittedBox, computedfontSize, orTextOverflow.ellipsisto make a label fit. They turn "doesn't fit at 200%" from a loud test failure into truncated or shrunk text on a device. Build flexible heights and let text wrap. - Never encode state through color alone. Pair every stateful signal with at least one non-color channel — icon + text label + shape/pattern + position. Color-only state is invisible under
invertColors, grayscale color-correction, and to every screen-reader user. - Contrast is measured against the composited background in both themes — body/icon text ≥ 4.5:1 (AA 1.4.3), large/hero text ≥ 3:1 (AA) — large text never has to clear a higher bar than body. Never test against the nominal token color when a fill, gradient, or translucency sits between text and surface.
- Every interactive target is ≥ 44×44 logical pixels and single-tap. No long-press or precise-gesture-only affordances; hand-off and motor-impaired use demand a large forgiving target.
- Traversal order is authored with
sortKey, never inherited from layout — visual position optimized for the thumb actively pessimises linear screen-reader/switch scanning. - Honor
boldText; keep haptics through reduced motion. HardcodingfontWeightthrows the setting away. ReaddisableAnimationsfromMediaQueryhere, but resolve how motion collapses throughdesign-system-structure's reduced-motion token +resolveMotionhelper (that skill owns the mechanism) — an animation that still conveys meaning with no reduced-motion path is a bug.
Where a11y state comes from
Read platform accessibility state from context, app state from your notifier:
@override
Widget build(BuildContext context) {
final boldText = MediaQuery.boldTextOf(context);
final highContrast = MediaQuery.highContrastOf(context);
final reduceMotion = MediaQuery.disableAnimationsOf(context);
// textScaler is deliberately NOT read and NOT clamped. Text scales itself.
...
}MediaQuery.highContrastOf is iOS-only and always false on Android (AccessibilityFeatures.highContrast is documented "Only supported on iOS"). Read the flag opportunistically; never gate anything on it, and provide an in-app contrast/theme control as the mechanism that works everywhere. invertColors is a system compositing filter — never reimplement it; just never encode meaning in color alone, or an inverted screen changes what the UI says.
The tap target, wrong and right
// WRONG — invisible to a screen reader, a clamping hack, a hidden overflow bug
class ItemTile extends StatelessWidget {
const ItemTile({super.key, required this.item, required this.onTap});
final Item item;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return MediaQuery.withClampedTextScaling( // (1) BANNED clamp
maxScaleFactor: 1.3,
child: GestureDetector(
onTap: onTap,
child: Column(
children: [
const Icon(Icons.star), // (2) no semanticLabel
Text(
item.title, // (3) no Semantics button role
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.normal, // (4) ignores boldText
),
overflow: TextOverflow.ellipsis, // (5) HIDES the overflow bug
),
],
),
),
);
}
}// RIGHT — semantics authored, text free to scale, target ≥ 44px, non-color state
class ItemTile extends StatelessWidget {
const ItemTile({super.key, required this.item, required this.onToggle});
final Item item;
final void Function(ItemId id) onToggle; // resolve at tap from the stable id
@override
Widget build(BuildContext context) {
final bold = MediaQuery.boldTextOf(context);
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context); // non-null getter (i18n-rtl-l10n owns this contract)
return Semantics(
container: true,
button: true,
label: item.title, // display label, not a sentence
// Non-color state: read out AND flip an icon (below), never color alone.
value: item.isSelected ? l10n.selected : l10n.notSelected, // localized, not literal
sortKey: OrdinalSortKey(item.priority.toDouble()), // priority, not layout
child: GestureDetector(
behavior: HitTestBehavior.opaque, // whole cell is the target
onTap: () => onToggle(item.id), // stable id, no stale-closure capture
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
child: ExcludeSemantics( // face already announced above
child: Row(
children: [
Icon(item.isSelected ? Icons.check_circle : Icons.circle_outlined),
Text(
item.title, // no ellipsis, no FittedBox
style: theme.textTheme.bodyLarge!.copyWith(
fontWeight: bold ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
),
),
);
}
}The label is the display label, never a longer vocalization — nothing in the type system distinguishes the two Strings, so a scanning user hears the whole sentence on every step unless you keep them separate. Resolve the action from the immutable id at tap time; capturing a mutable field into the closure fires the stale value on a fast re-tap.
Text scale: the instinct is the bug
The reflex when a fixed layout overflows at 200% is to disable text scaling for stability. That reflex is the defect. Auto-shrinking — FittedBox, computed fontSize, maxLines + ellipsis — is the identical bug in disguise: the layout stays tidy and the user's setting stops working, silently, with every guideline still green.
- Honor
MediaQuery.textScalerOf(context)by not touching it. Flutter text widgets already scale. textScaleFactoris deprecated in favour ofTextScalerprecisely to support Android's nonlinear scaling; its use is almost always a clamp.- Build intrinsic/flexible heights; let text wrap to as many lines as it needs. Hero and display sizes may cap their own growth and wrap to a second line rather than truncate.
- Reflow horizontal rows (chip bars, toolbars) at accessibility text sizes instead of clipping.
- Use
FontFeature.tabularFigures()(or.monospacedDigit()-style tabular numerals) wherever numbers change or align so they don't reflow or jitter. - Let overflow scream in tests. A red/yellow overflow stripe in a widget test is the feedback loop; a truncated word on a device is a failure nobody reports.
Never state through color alone
A real state — selected, active, pending, complete, error, disabled — is knowable without color. Encode it redundantly across independent channels:
- Icon / glyph whose shape differs per state (grayscale-legible).
- Text label — the word for the state, routed through localization.
- Shape / pattern / outline — a monotonic stripe, keyline, or notch.
- Position — sort the item that needs attention to the top under a header.
Expose the state to assistive tech through Semantics — a value/hint, or a liveRegion for a transient announcement — never through a luminance step alone:
Semantics(
liveRegion: true, // announced when it changes, e.g. "Sync complete"
label: statusLabel(status), // the word, not the color
child: StatusBadge(status: status), // icon + label + shape, color is decoration
)Pair visual feedback with HapticFeedback.selectionClick() so confirmation survives reduced motion and low vision. Guard any latched "active" visual with a timeout that force-clears it — a stuck state is both a visual lie and a stale semantic value.
Contrast against the composited background
Verify contrast against what the pixel actually composites to, in both light and dark, not against the nominal token:
- Body text and meaningful icons/outlines ≥ 4.5:1 (AA 1.4.3); push to 7:1 (AAA 1.4.6) for body where you can reach it. Large/hero text ≥ 3:1 (AA) or 4.5:1 (AAA) — large text is never required to exceed body.
- Never render a critical number or label on a gradient, translucent glass, or chip fill; put it on an opaque surface.
- Any accent fill that carries text must be pre-paired with a foreground that clears 4.5:1 on that fill, re-verified in dark mode (dark values are hand-tuned, never auto-flipped).
- Avoid the red/green trap: a neutral value is neutral ink, "success" is green + ✓ glyph + the word, danger-red appears only on destructive actions.
- Verify the whole UI in a grayscale pass and deuteranopia / protanopia / tritanopia simulation.
Traversal order is a design decision
Layout optimized for thumb reach (highest-priority controls low and central) combined with Flutter's default row-major traversal makes the most important control the last thing a linear scanner reaches. Decouple traversal from position with one argument:
Semantics(
sortKey: OrdinalSortKey(item.priority.toDouble()), // authored from priority
... )Assert sortKey order equals priority order (not layout order) in a test. Know the limit: Flutter publishes no Switch Access support statement and no API simulates scanning or group selection, so this is a regression guard on intent — real-device passes with TalkBack, VoiceOver, and Switch Access are the only conformance evidence. Touch, switch, and screen reader are three different channels: design for all three or state plainly which one was dropped.
Anti-patterns
MediaQuery.withClampedTextScaling/textScaleFactor— the one-line "fix" a future contributor reaches for when an overflow test goes red; it defeats the entire text-scale matrix silently. Ban withgrep -rn "withClampedTextScaling\|textScaleFactor" lib/in CI.FittedBox/ computedfontSize/ellipsison a real label — same bug wearing a disguise; the setting stops working while the layout stays tidy.- Unlabelled
Icon/Image— invisible to a screen reader; must be labelled orExcludeSemantics. - Reading
boldText/highContrast/disableAnimationsfrom a provider — one frame stale, in the area where wrong is total failure. - Gating anything on
highContrastOf— always false on Android; it silently disables your feature on the majority platform. - Color as the only channel for a state — dies under
invertColors, grayscale mode, and screen readers. - Capturing a mutable field into an
onTapclosure — fires the stale value on a fast re-tap; resolve from the stable id at tap time. - A precise-gesture-only or long-press-only affordance — excludes motor-impaired and hand-off use; provide a ≥44px single tap.
containsSemantics(...)in tests — deprecated; useisSemantics(...).
Definition of done
For any widget with a tap target or state:
Semanticsnode withbutton: true, a display-onlylabel, and an authoredsortKey.- Every
Icon/Imagelabelled or explicitlyExcludeSemantics. - No clamp, no
FittedBox, noellipsison a real label; renders atTextScaler.linear(2.0)without overflow. boldTexthonored; no hardcodedfontWeighton user-facing text.- Every state carries a non-color channel; transient state uses
liveRegionorvalue; feedback includes a haptic. - Contrast verified against the composited background in both themes (≥4.5:1 body / ≥3:1 large) and in a grayscale + CVD pass.
- Interactive targets ≥ 44×44 and single-tap.
- A test asserting the semantics with
isSemantics(...)and traversal against priority order.
Related skills
widget-golden-and-a11y-testing— the guideline matchers, golden lanes, RTL goldens, and honest a11y-conformance limits that verify this authoring.i18n-rtl-l10n— state labels and announcements routed through gen-l10n/ARB, and Directional geometry for correct-by-construction RTL.design-system-structure— token→theme layering, hand-authoredColorScheme, the no-raw-values gate that keeps contrast pairings honest, and the reduced-motion token +resolveMotionhelper it owns (this skill reads the flag, that skill resolves the motion).state-management-riverpod— the split both skills co-own: app/domain state flows through Riverpod notifiers, while platform/a11y flags are read fromMediaQuery/BuildContextand never routed through a provider.widget-composition— small const Views, dumb widgets, and dispose discipline that this semantics work sits on top of.design-review-workflow— the end-of-build screenshot sweep (light/dark × RTL × largest text × reduce-motion) where a11y floors are always blocking.
References
- Flutter accessibility: https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility
Semanticswidget: https://api.flutter.dev/flutter/widgets/Semantics-class.htmlMediaQueryDataaccessibility flags: https://api.flutter.dev/flutter/widgets/MediaQueryData-class.htmlTextScaler: https://api.flutter.dev/flutter/painting/TextScaler-class.htmlOrdinalSortKey: https://api.flutter.dev/flutter/semantics/OrdinalSortKey-class.html- WCAG 2.2 contrast (1.4.3 / 1.4.6): https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

