accessibility-as-code
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAccessibility 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: and ship zero a11y rules. Enforcement is the widget itself, review of your own diff, and widget tests. Applies to every interactive View, every /, every state, and every color choice.
flutter_lintsvery_good_analysisIconImageTesting mechanics (guideline matchers, golden lanes, RTL, honest conformance limits) live in ; this skill is the authoring discipline.
widget-golden-and-a11y-testing无障碍应在编写 widget 时融入其中——与“按钮确实能正常工作”处于同一层级。这一整套内容都没有相应的 lint: 和 都没有提供任何 a11y 规则。约束手段是 widget 本身、对自身 diff 的审查以及 widget 测试。它适用于每个交互式 View、每个 /、每种状态和每一种颜色选择。
flutter_lintsvery_good_analysisIconImage测试机制(guideline matchers、golden lanes、RTL,以及诚实的符合性边界)位于 ;本 skill 关注的是编写规范。
widget-golden-and-a11y-testingNon-negotiable rules
不可妥协的规则
- Every interactive node gets — a raw
Semantics(button: true, label: ...)with 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.GestureDetector - Every /
Icongets aImage, or is wrapped insemanticLabelbecause it is decorative. There is no third option — an unlabelledExcludeSemanticsis invisible to a screen reader.Icon - Read a11y state from at build time, never from app state.
MediaQueryis anMediaQuerywith 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.InheritedWidget - Never , never
MediaQuery.withClampedTextScaling. 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).textScaleFactor - Never , computed
FittedBox, orfontSizeto 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.TextOverflow.ellipsis - 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 , grayscale color-correction, and to every screen-reader user.
invertColors - 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 , never inherited from layout — visual position optimized for the thumb actively pessimises linear screen-reader/switch scanning.
sortKey - Honor ; keep haptics through reduced motion. Hardcoding
boldTextthrows the setting away. ReadfontWeightfromdisableAnimationshere, but resolve how motion collapses throughMediaQuery's reduced-motion token +design-system-structurehelper (that skill owns the mechanism) — an animation that still conveys meaning with no reduced-motion path is a bug.resolveMotion
- 每个交互节点都应添加 ——没有语义的原始
Semantics(button: true, label: ...)会悄无声息地将所有使用屏幕阅读器或开关设备的用户挡在门外。正确的语义可让 TalkBack、VoiceOver、Switch Access 和 Switch Control 无需额外工作即可使用;它是完成定义,绝不能成为 backlog 中的待办项。GestureDetector - **每个 /
Icon都应提供Image,或者由semanticLabel明确包裹,因为它属于装饰性内容。不存在第三种选择——未标注的ExcludeSemantics对屏幕阅读器而言不存在。Icon - 在 build 时从 读取 a11y 状态,绝不能从 app state 读取。
MediaQuery是一个MediaQuery,其失效机制从构造上就能保证正确;把这类数据经由 provider 传递,相当于用编译器可保证的 rebuild 换取一帧陈旧数据,而在这个领域,出错就意味着彻底失败。InheritedWidget - 绝不要使用 ,也绝不要使用
MediaQuery.withClampedTextScaling。 二者都是文本缩放钳制 hack,会悄悄破坏文本缩放矩阵,同时让对比度和点击目标规范仍然显示为通过。应通过 source grep 强制禁止(参见“反模式”)。textScaleFactor - 绝不要使用 、计算得出的
FittedBox或fontSize来让标签适配。 它们会把“200% 下放不下”从醒目的测试失败变成设备上被截断或缩小的文本。请构建灵活的高度,并允许文本换行。TextOverflow.ellipsis - 绝不能仅通过颜色编码状态。 每个有状态信号都至少搭配一种非颜色通道——icon + text label + shape/pattern + position。仅靠颜色表达的状态在 、灰度色彩校正以及所有屏幕阅读器用户面前都会消失。
invertColors - 对比度必须针对两种主题下实际合成后的背景进行测量——正文/icon 文本 ≥ 4.5:1(AA 1.4.3),大号/hero 文本 ≥ 3:1(AA)——大号文本绝不需要满足比正文更高的标准。当文本与 surface 之间存在填充、渐变或半透明层时,绝不能只针对名义上的 token 颜色进行测试。
- 每个交互目标都必须 ≥ 44×44 逻辑像素,并且可通过单次点按完成。 不得提供只能通过长按或精确手势操作的交互方式;对于需要照护者协助或存在运动障碍的用户,必须使用更大且更宽容的目标。
- 遍历顺序必须通过 明确编写,绝不能从布局中继承——为拇指操作优化的视觉位置,实际上会恶化屏幕阅读器/开关设备的线性扫描顺序。
sortKey - 遵循 ;在减少动态效果时仍保留触觉反馈。 硬编码
boldText会直接丢弃该设置。此处从fontWeight读取MediaQuery,但应通过disableAnimations的 reduced-motion token 与design-system-structurehelper 决定动态效果如何降级(该 skill 负责具体机制)——如果动画在没有任何 reduced-motion 路径时仍用于传达含义,那就是缺陷。resolveMotion
Where a11y state comes from
a11y 状态的来源
Read platform accessibility state from , app state from your notifier:
contextdart
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.highContrastOfAccessibilityFeatures.highContrastinvertColors从 读取平台无障碍状态,从 notifier 读取 app state:
contextdart
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.highContrastOfAccessibilityFeatures.highContrastinvertColorsThe tap target, wrong and right
点击目标:错误与正确示例
dart
// 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;
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
),
],
),
),
);
}
}dart
// 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
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 is the display label, never a longer vocalization — nothing in the type system distinguishes the two s, 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.
labelStringdart
// 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;
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
),
],
),
),
);
}
}dart
// 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
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,
),
),
],
),
),
),
),
);
}
}labelStringText 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 — , computed , + — is the identical bug in disguise: the layout stays tidy and the user's setting stops working, silently, with every guideline still green.
FittedBoxfontSizemaxLinesellipsis- Honor by not touching it. Flutter text widgets already scale.
MediaQuery.textScalerOf(context) - is deprecated in favour of
textScaleFactorprecisely to support Android's nonlinear scaling; its use is almost always a clamp.TextScaler - 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 (or
FontFeature.tabularFigures()-style tabular numerals) wherever numbers change or align so they don't reflow or jitter..monospacedDigit() - 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.
当固定布局在 200% 下溢出时,第一反应通常是禁用文本缩放以保持稳定。这个反应本身就是缺陷。自动缩小——、计算得出的 、 + ——只是同一缺陷换了一副伪装:布局依然整洁,用户的设置却悄无声息地失效,同时所有规范检查仍然显示为绿色。
FittedBoxfontSizemaxLinesellipsis- 通过完全不修改 来遵循它。Flutter 文本 widget 本来就会缩放。
MediaQuery.textScalerOf(context) - 已被
textScaleFactor取代,正是因为后者支持 Android 的非线性缩放;使用TextScaler几乎总是为了进行钳制。textScaleFactor - 构建固有/灵活高度;允许文本按需要换行到任意多行。Hero 和 display 字号可以限制自身的增长幅度,但应当换行到第二行,而不是截断。
- 在无障碍文本尺寸下重新排列横向行(chip 栏、工具栏),而不是裁剪内容。
- 在数字会变化或需要对齐的位置使用 (或类似
FontFeature.tabularFigures()的等宽数字),避免文本重排或抖动。.monospacedDigit() - 让溢出在测试中大声报错。widget 测试中的红/黄色溢出条才是反馈闭环;设备上一个被截断的单词则是无人报告的失败。
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 — a /, or a for a transient announcement — never through a luminance step alone:
SemanticsvaluehintliveRegiondart
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 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.
HapticFeedback.selectionClick()选中、激活、待处理、完成、错误、禁用等真实状态,都必须能够在不依赖颜色的情况下被识别。请通过彼此独立的通道进行冗余编码:
- Icon / glyph:其形状应随状态变化,并能够在灰度下辨识。
- Text label:使用表示该状态的词语,并通过 localization 路由。
- Shape / pattern / outline:使用单色条纹、keyline 或缺口等视觉特征。
- Position:把需要关注的条目排在标题下方最靠前的位置。
通过 向辅助技术暴露状态——使用 /,或为瞬态播报使用 ——绝不能只通过亮度变化来表达:
SemanticsvaluehintliveRegiondart
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
)将视觉反馈与 配对,使确认反馈在 reduced motion 和低视力环境下仍然有效。对于任何会锁定的“active”视觉状态,应使用 timeout 强制清除——卡住的状态既是视觉上的谎言,也是陈旧的语义值。
HapticFeedback.selectionClick()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.
应在浅色和深色两种主题中,针对像素实际合成后的结果验证对比度,而不是针对名义上的 token:
- 正文文本以及承载含义的 icon/描边 ≥ 4.5:1(AA 1.4.3);在条件允许时,正文应努力达到 7:1(AAA 1.4.6)。大号/hero 文本 ≥ 3:1(AA)或 4.5:1(AAA)——大号文本绝不会被要求达到高于正文的标准。
- 绝不要把关键数字或标签渲染在渐变、半透明玻璃或 chip 填充上;应将其放在不透明 surface 上。
- 任何承载文本的 accent fill,都必须预先与在该填充上达到 4.5:1 的 foreground 配对,并在深色模式下重新验证(深色数值应手动调整,绝不能自动翻转)。
- 避免红/绿陷阱:中性值应使用中性 ink;“success”应同时使用绿色 + ✓ glyph + 对应词语;danger-red 只应出现在破坏性操作上。
- 应对整个 UI 执行灰度检查以及 deuteranopia / protanopia / tritanopia 模拟。
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:
dart
Semantics(
sortKey: OrdinalSortKey(item.priority.toDouble()), // authored from priority
... )Assert 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.
sortKey为拇指可达性优化的布局(将最高优先级控件放在靠下且居中的位置),结合 Flutter 默认的行优先遍历顺序,可能使最重要的控件成为线性扫描器最后才到达的对象。通过以下一个参数将遍历顺序与位置解耦:
dart
Semantics(
sortKey: OrdinalSortKey(item.priority.toDouble()), // authored from priority
...)在测试中断言 顺序等于优先级顺序(而非布局顺序)。了解其边界:Flutter 没有发布关于 Switch Access 支持情况的声明,也没有用于模拟扫描或分组选择的 API,因此这只能作为对设计意图的回归保护——在真实设备上通过 TalkBack、VoiceOver 和 Switch Access 验证,才是唯一的符合性证据。触控、开关和屏幕阅读器是三种不同的通道:必须为三者全部进行设计,或者明确说明舍弃了其中哪一种。
sortKeyAnti-patterns
反模式
- /
MediaQuery.withClampedTextScaling— the one-line "fix" a future contributor reaches for when an overflow test goes red; it defeats the entire text-scale matrix silently. Ban withtextScaleFactorin CI.grep -rn "withClampedTextScaling\|textScaleFactor" lib/ - / computed
FittedBox/fontSizeon a real label — same bug wearing a disguise; the setting stops working while the layout stays tidy.ellipsis - Unlabelled /
Icon— invisible to a screen reader; must be labelled orImage.ExcludeSemantics - Reading /
boldText/highContrastfrom a provider — one frame stale, in the area where wrong is total failure.disableAnimations - Gating anything on — always false on Android; it silently disables your feature on the majority platform.
highContrastOf - Color as the only channel for a state — dies under , grayscale mode, and screen readers.
invertColors - Capturing a mutable field into an closure — fires the stale value on a fast re-tap; resolve from the stable id at tap time.
onTap - A precise-gesture-only or long-press-only affordance — excludes motor-impaired and hand-off use; provide a ≥44px single tap.
- in tests — deprecated; use
containsSemantics(...).isSemantics(...)
- /
MediaQuery.withClampedTextScaling——当溢出测试变红时,未来贡献者最容易采用这种“一行修复”;它会在悄无声息中破坏整个文本缩放矩阵。应通过textScaleFactor在 CI 中禁止。grep -rn "withClampedTextScaling\|textScaleFactor" lib/ - 对真实标签使用 / 计算得出的
FittedBox/fontSize——同一缺陷换了一副伪装;布局依然整洁,用户的设置却悄无声息地失效。ellipsis - 未标注的 /
Icon——对屏幕阅读器不可见;必须标注或使用Image。ExcludeSemantics - 从 provider 读取 /
boldText/highContrast——会落后一帧,而在这个领域,错误就意味着彻底失败。disableAnimations - 基于 对任何功能设限——它在 Android 上始终为 false;它会在大多数平台上悄无声息地禁用你的功能。
highContrastOf - 仅使用颜色作为状态通道——在 、灰度模式和屏幕阅读器下都会失效。
invertColors - 把可变字段捕获到 闭包中——快速重复点击时会触发陈旧值;应在点击时根据稳定 id 解析。
onTap - 只能通过精确手势或长按操作的交互方式——会排斥运动障碍用户以及需要照护者协助的使用场景;应提供 ≥ 44px 的单次点按目标。
- 在测试中使用 ——已弃用;请使用
containsSemantics(...)。isSemantics(...)
Definition of done
完成定义
For any widget with a tap target or state:
- node with
Semantics, a display-onlybutton: true, and an authoredlabel.sortKey - Every /
Iconlabelled or explicitlyImage.ExcludeSemantics - No clamp, no , no
FittedBoxon a real label; renders atellipsiswithout overflow.TextScaler.linear(2.0) - honored; no hardcoded
boldTexton user-facing text.fontWeight - Every state carries a non-color channel; transient state uses or
liveRegion; feedback includes a haptic.value - 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 and traversal against priority order.
isSemantics(...)
对于任何包含点击目标或状态的 widget:
- 包含 节点,设置
Semantics、仅用于显示的button: true,并明确编写label。sortKey - 每个 /
Icon都已标注,或被明确包裹在Image中。ExcludeSemantics - 不使用钳制、,也不在真实标签上使用
FittedBox;能够以ellipsis渲染且不溢出。TextScaler.linear(2.0) - 遵循 ;面向用户的文本不得硬编码
boldText。fontWeight - 每种状态都具备非颜色通道;瞬态状态使用 或
liveRegion;反馈包含触觉反馈。value - 在两种主题中针对合成后的背景验证对比度(正文 ≥ 4.5:1 / 大号文本 ≥ 3:1),并完成灰度与 CVD 检查。
- 交互目标 ≥ 44×44,并可通过单次点按完成。
- 使用 断言语义,并测试遍历顺序是否与优先级顺序一致。
isSemantics(...)
Related skills
相关 skill
- — the guideline matchers, golden lanes, RTL goldens, and honest a11y-conformance limits that verify this authoring.
widget-golden-and-a11y-testing - — state labels and announcements routed through gen-l10n/ARB, and Directional geometry for correct-by-construction RTL.
i18n-rtl-l10n - — token→theme layering, hand-authored
design-system-structure, the no-raw-values gate that keeps contrast pairings honest, and the reduced-motion token +ColorSchemehelper it owns (this skill reads the flag, that skill resolves the motion).resolveMotion - — the split both skills co-own: app/domain state flows through Riverpod notifiers, while platform/a11y flags are read from
state-management-riverpod/MediaQueryand never routed through a provider.BuildContext - — small const Views, dumb widgets, and dispose discipline that this semantics work sits on top of.
widget-composition - — the end-of-build screenshot sweep (light/dark × RTL × largest text × reduce-motion) where a11y floors are always blocking.
design-review-workflow
- ——用于验证上述编写规范的 guideline matchers、golden lanes、RTL goldens,以及诚实的 a11y 符合性边界。
widget-golden-and-a11y-testing - ——通过 gen-l10n/ARB 路由状态标签和播报,并使用 Directional 几何实现从构造上保证正确的 RTL。
i18n-rtl-l10n - ——token→theme 分层、手工编写的
design-system-structure、用于保持对比度配对诚实的禁止原始值 gate,以及该 skill 所负责的 reduced-motion token +ColorSchemehelper(本 skill 读取标志,该 skill 负责解析动态效果)。resolveMotion - ——两个 skill 共同负责的边界:app/domain state 经由 Riverpod notifier 流动,而平台/a11y 标志从
state-management-riverpod/MediaQuery读取,绝不通过 provider 路由。BuildContext - ——小型 const View、简单 widget 和 dispose 管理规范;上述语义工作建立在此基础之上。
widget-composition - ——构建结束后的截图巡检(浅色/深色 × RTL × 最大文本 × reduce-motion),其中无障碍最低标准始终具有阻断作用。
design-review-workflow
References
参考资料
- Flutter accessibility: https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility
- widget: https://api.flutter.dev/flutter/widgets/Semantics-class.html
Semantics - accessibility flags: https://api.flutter.dev/flutter/widgets/MediaQueryData-class.html
MediaQueryData - : https://api.flutter.dev/flutter/painting/TextScaler-class.html
TextScaler - : https://api.flutter.dev/flutter/semantics/OrdinalSortKey-class.html
OrdinalSortKey - WCAG 2.2 contrast (1.4.3 / 1.4.6): https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
- Flutter 无障碍:https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility
- widget:https://api.flutter.dev/flutter/widgets/Semantics-class.html
Semantics - 无障碍标志:https://api.flutter.dev/flutter/widgets/MediaQueryData-class.html
MediaQueryData - :https://api.flutter.dev/flutter/painting/TextScaler-class.html
TextScaler - :https://api.flutter.dev/flutter/semantics/OrdinalSortKey-class.html
OrdinalSortKey - WCAG 2.2 对比度(1.4.3 / 1.4.6):https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html