accessibility-as-code

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.
无障碍应在编写 widget 时融入其中——与“按钮确实能正常工作”处于同一层级。这一整套内容都没有相应的 lint:
flutter_lints
和
very_good_analysis
都没有提供任何 a11y 规则。约束手段是 widget 本身、对自身 diff 的审查以及 widget 测试。它适用于每个交互式 View、每个
Icon
/
Image
、每种状态和每一种颜色选择。
测试机制(guideline matchers、golden lanes、RTL,以及诚实的符合性边界)位于
widget-golden-and-a11y-testing
;本 skill 关注的是编写规范。

Non-negotiable rules

不可妥协的规则

  1. Every interactive node gets
    Semantics(button: true, label: ...)
    — a raw
    GestureDetector
    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.
  2. Every
    Icon
    /
    Image
    gets a
    semanticLabel
    , or is wrapped in
    ExcludeSemantics
    because it is decorative. There is no third option — an unlabelled
    Icon
    is invisible to a screen reader.
  3. Read a11y state from
    MediaQuery
    at build time, never from app state.
    MediaQuery
    is an
    InheritedWidget
    with 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.
  4. Never
    MediaQuery.withClampedTextScaling
    , never
    textScaleFactor
    .
    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).
  5. Never
    FittedBox
    , computed
    fontSize
    , or
    TextOverflow.ellipsis
    to 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.
  6. 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.
  7. 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.
  8. 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.
  9. Traversal order is authored with
    sortKey
    , never inherited from layout
    — visual position optimized for the thumb actively pessimises linear screen-reader/switch scanning.
  10. Honor
    boldText
    ; keep haptics through reduced motion.
    Hardcoding
    fontWeight
    throws the setting away. Read
    disableAnimations
    from
    MediaQuery
    here, but resolve how motion collapses through
    design-system-structure
    's reduced-motion token +
    resolveMotion
    helper (that skill owns the mechanism) — an animation that still conveys meaning with no reduced-motion path is a bug.
  1. 每个交互节点都应添加
    Semantics(button: true, label: ...)
    ——没有语义的原始
    GestureDetector
    会悄无声息地将所有使用屏幕阅读器或开关设备的用户挡在门外。正确的语义可让 TalkBack、VoiceOver、Switch Access 和 Switch Control 无需额外工作即可使用;它是完成定义,绝不能成为 backlog 中的待办项。
  2. **每个
    Icon
    /
    Image
    都应提供
    semanticLabel
    ,或者由
    ExcludeSemantics
    明确包裹,因为它属于装饰性内容。不存在第三种选择——未标注的
    Icon
    对屏幕阅读器而言不存在。
  3. 在 build 时从
    MediaQuery
    读取 a11y 状态,绝不能从 app state 读取。
    MediaQuery
    是一个
    InheritedWidget
    ,其失效机制从构造上就能保证正确;把这类数据经由 provider 传递,相当于用编译器可保证的 rebuild 换取一帧陈旧数据,而在这个领域,出错就意味着彻底失败。
  4. 绝不要使用
    MediaQuery.withClampedTextScaling
    ,也绝不要使用
    textScaleFactor
    。
    二者都是文本缩放钳制 hack,会悄悄破坏文本缩放矩阵,同时让对比度和点击目标规范仍然显示为通过。应通过 source grep 强制禁止(参见“反模式”)。
  5. 绝不要使用
    FittedBox
    、计算得出的
    fontSize
    或
    TextOverflow.ellipsis
    来让标签适配。
    它们会把“200% 下放不下”从醒目的测试失败变成设备上被截断或缩小的文本。请构建灵活的高度,并允许文本换行。
  6. 绝不能仅通过颜色编码状态。 每个有状态信号都至少搭配一种非颜色通道——icon + text label + shape/pattern + position。仅靠颜色表达的状态在
    invertColors
    、灰度色彩校正以及所有屏幕阅读器用户面前都会消失。
  7. 对比度必须针对两种主题下实际合成后的背景进行测量——正文/icon 文本 ≥ 4.5:1(AA 1.4.3),大号/hero 文本 ≥ 3:1(AA)——大号文本绝不需要满足比正文更高的标准。当文本与 surface 之间存在填充、渐变或半透明层时,绝不能只针对名义上的 token 颜色进行测试。
  8. 每个交互目标都必须 ≥ 44×44 逻辑像素,并且可通过单次点按完成。 不得提供只能通过长按或精确手势操作的交互方式;对于需要照护者协助或存在运动障碍的用户,必须使用更大且更宽容的目标。
  9. 遍历顺序必须通过
    sortKey
    明确编写,绝不能从布局中继承
    ——为拇指操作优化的视觉位置,实际上会恶化屏幕阅读器/开关设备的线性扫描顺序。
  10. 遵循
    boldText
    ;在减少动态效果时仍保留触觉反馈。
    硬编码
    fontWeight
    会直接丢弃该设置。此处从
    MediaQuery
    读取
    disableAnimations
    ,但应通过
    design-system-structure
    的 reduced-motion token 与
    resolveMotion
    helper 决定动态效果如何降级(该 skill 负责具体机制)——如果动画在没有任何 reduced-motion 路径时仍用于传达含义,那就是缺陷。

Where a11y state comes from

a11y 状态的来源

Read platform accessibility state from
context
, app state from your notifier:
dart

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.
从
context
读取平台无障碍状态,从 notifier 读取 app state:
dart

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
仅支持 iOS,并且在 Android 上始终为 false(
AccessibilityFeatures.highContrast
的文档说明为 "Only supported on iOS")。可以择机读取该标志,但绝不能基于它对任何功能设限;应提供应用内对比度/主题控制,作为在所有平台上都有效的机制。
invertColors
是系统合成滤镜——绝不要自行重新实现;只需确保不单独使用颜色编码含义,否则反转后的屏幕会改变 UI 所表达的内容。

The 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
label
is the display label, never a longer vocalization — nothing in the type system distinguishes the two
String
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.
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,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
label
是显示标签,绝不能是更长的朗读内容——类型系统无法区分这两个
String
,因此除非将二者分开,否则扫描用户在每一步都会听到整句话。应在点击时根据不可变的 id 解析操作;如果把可变字段捕获到闭包中,快速重复点击时会触发陈旧值。

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.
  • textScaleFactor
    is deprecated in favour of
    TextScaler
    precisely 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.
当固定布局在 200% 下溢出时,第一反应通常是禁用文本缩放以保持稳定。这个反应本身就是缺陷。自动缩小——
FittedBox
、计算得出的
fontSize
、
maxLines
+
ellipsis
——只是同一缺陷换了一副伪装:布局依然整洁,用户的设置却悄无声息地失效,同时所有规范检查仍然显示为绿色。
  • 通过完全不修改
    MediaQuery.textScalerOf(context)
    来遵循它。Flutter 文本 widget 本来就会缩放。
  • textScaleFactor
    已被
    TextScaler
    取代,正是因为后者支持 Android 的非线性缩放;使用
    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
Semantics
— a
value
/
hint
, or a
liveRegion
for a transient announcement — never through a luminance step alone:
dart
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.
选中、激活、待处理、完成、错误、禁用等真实状态,都必须能够在不依赖颜色的情况下被识别。请通过彼此独立的通道进行冗余编码:
  • Icon / glyph:其形状应随状态变化,并能够在灰度下辨识。
  • Text label:使用表示该状态的词语,并通过 localization 路由。
  • Shape / pattern / outline:使用单色条纹、keyline 或缺口等视觉特征。
  • Position:把需要关注的条目排在标题下方最靠前的位置。
通过
Semantics
向辅助技术暴露状态——使用
value
/
hint
,或为瞬态播报使用
liveRegion
——绝不能只通过亮度变化来表达:
dart
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
)
将视觉反馈与
HapticFeedback.selectionClick()
配对,使确认反馈在 reduced motion 和低视力环境下仍然有效。对于任何会锁定的“active”视觉状态,应使用 timeout 强制清除——卡住的状态既是视觉上的谎言,也是陈旧的语义值。

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
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.
为拇指可达性优化的布局(将最高优先级控件放在靠下且居中的位置),结合 Flutter 默认的行优先遍历顺序,可能使最重要的控件成为线性扫描器最后才到达的对象。通过以下一个参数将遍历顺序与位置解耦:
dart
Semantics(
  sortKey: OrdinalSortKey(item.priority.toDouble()), // authored from priority
  ...)
在测试中断言
sortKey
顺序等于优先级顺序(而非布局顺序)。了解其边界:Flutter 没有发布关于 Switch Access 支持情况的声明,也没有用于模拟扫描或分组选择的 API,因此这只能作为对设计意图的回归保护——在真实设备上通过 TalkBack、VoiceOver 和 Switch Access 验证,才是唯一的符合性证据。触控、开关和屏幕阅读器是三种不同的通道:必须为三者全部进行设计,或者明确说明舍弃了其中哪一种。

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 with
    grep -rn "withClampedTextScaling\|textScaleFactor" lib/
    in CI.
  • FittedBox
    / computed
    fontSize
    /
    ellipsis
    on 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 or
    ExcludeSemantics
    .
  • Reading
    boldText
    /
    highContrast
    /
    disableAnimations
    from 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
    onTap
    closure
    — 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; use
    isSemantics(...)
    .
  • MediaQuery.withClampedTextScaling
    /
    textScaleFactor
    ——当溢出测试变红时,未来贡献者最容易采用这种“一行修复”;它会在悄无声息中破坏整个文本缩放矩阵。应通过
    grep -rn "withClampedTextScaling\|textScaleFactor" lib/
    在 CI 中禁止。
  • 对真实标签使用
    FittedBox
    / 计算得出的
    fontSize
    /
    ellipsis
    ——同一缺陷换了一副伪装;布局依然整洁,用户的设置却悄无声息地失效。
  • 未标注的
    Icon
    /
    Image
    ——对屏幕阅读器不可见;必须标注或使用
    ExcludeSemantics
    。
  • 从 provider 读取
    boldText
    /
    highContrast
    /
    disableAnimations
    ——会落后一帧,而在这个领域,错误就意味着彻底失败。
  • 基于
    highContrastOf
    对任何功能设限
    ——它在 Android 上始终为 false;它会在大多数平台上悄无声息地禁用你的功能。
  • 仅使用颜色作为状态通道——在
    invertColors
    、灰度模式和屏幕阅读器下都会失效。
  • 把可变字段捕获到
    onTap
    闭包中
    ——快速重复点击时会触发陈旧值;应在点击时根据稳定 id 解析。
  • 只能通过精确手势或长按操作的交互方式——会排斥运动障碍用户以及需要照护者协助的使用场景;应提供 ≥ 44px 的单次点按目标。
  • 在测试中使用
    containsSemantics(...)
    ——已弃用;请使用
    isSemantics(...)
    。

Definition of done

完成定义

For any widget with a tap target or state:
  1. Semantics
    node with
    button: true
    , a display-only
    label
    , and an authored
    sortKey
    .
  2. Every
    Icon
    /
    Image
    labelled or explicitly
    ExcludeSemantics
    .
  3. No clamp, no
    FittedBox
    , no
    ellipsis
    on a real label; renders at
    TextScaler.linear(2.0)
    without overflow.
  4. boldText
    honored; no hardcoded
    fontWeight
    on user-facing text.
  5. Every state carries a non-color channel; transient state uses
    liveRegion
    or
    value
    ; feedback includes a haptic.
  6. Contrast verified against the composited background in both themes (≥4.5:1 body / ≥3:1 large) and in a grayscale + CVD pass.
  7. Interactive targets ≥ 44×44 and single-tap.
  8. A test asserting the semantics with
    isSemantics(...)
    and traversal against priority order.
对于任何包含点击目标或状态的 widget:
  1. 包含
    Semantics
    节点,设置
    button: true
    、仅用于显示的
    label
    ,并明确编写
    sortKey
    。
  2. 每个
    Icon
    /
    Image
    都已标注,或被明确包裹在
    ExcludeSemantics
    中。
  3. 不使用钳制、
    FittedBox
    ,也不在真实标签上使用
    ellipsis
    ;能够以
    TextScaler.linear(2.0)
    渲染且不溢出。
  4. 遵循
    boldText
    ;面向用户的文本不得硬编码
    fontWeight
    。
  5. 每种状态都具备非颜色通道;瞬态状态使用
    liveRegion
    或
    value
    ;反馈包含触觉反馈。
  6. 在两种主题中针对合成后的背景验证对比度(正文 ≥ 4.5:1 / 大号文本 ≥ 3:1),并完成灰度与 CVD 检查。
  7. 交互目标 ≥ 44×44,并可通过单次点按完成。
  8. 使用
    isSemantics(...)
    断言语义,并测试遍历顺序是否与优先级顺序一致。

Related skills

相关 skill

  • 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-authored
    ColorScheme
    , the no-raw-values gate that keeps contrast pairings honest, and the reduced-motion token +
    resolveMotion
    helper 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 from
    MediaQuery
    /
    BuildContext
    and 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.
  • widget-golden-and-a11y-testing
    ——用于验证上述编写规范的 guideline matchers、golden lanes、RTL goldens,以及诚实的 a11y 符合性边界。
  • i18n-rtl-l10n
    ——通过 gen-l10n/ARB 路由状态标签和播报,并使用 Directional 几何实现从构造上保证正确的 RTL。
  • design-system-structure
    ——token→theme 分层、手工编写的
    ColorScheme
    、用于保持对比度配对诚实的禁止原始值 gate,以及该 skill 所负责的 reduced-motion token +
    resolveMotion
    helper(本 skill 读取标志,该 skill 负责解析动态效果)。
  • state-management-riverpod
    ——两个 skill 共同负责的边界:app/domain state 经由 Riverpod notifier 流动,而平台/a11y 标志从
    MediaQuery
    /
    BuildContext
    读取,绝不通过 provider 路由。
  • widget-composition
    ——小型 const View、简单 widget 和 dispose 管理规范;上述语义工作建立在此基础之上。
  • design-review-workflow
    ——构建结束后的截图巡检(浅色/深色 × RTL × 最大文本 × reduce-motion),其中无障碍最低标准始终具有阻断作用。

References

参考资料