Skip to content
Back to skills

Flutter Accessibility

ASecurity

Use when implementing or auditing accessibility on a Flutter screen — the Semantics widget, screen-reader labels (TalkBack/VoiceOver), colour contrast, minimum tap-target size, system font scaling, or semantics in a web build. Also triggers on "make this accessible", "a11y", or a request for an accessibility review.

  • 11 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
developmentgobashtestinggitperformance

Works with

  • cli

Security analysis

A100/100

Scanned September 19, 2026

npx -y skills add gabuldev/flutter-skills --skill flutter-accessibility --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flutter Accessibility?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flutter Accessibility
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gabuldev-flutter-accessibility/badge)](https://www.skillsdirectory.com/skills/gabuldev-flutter-accessibility)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flutter-accessibility
description: Use when implementing or auditing accessibility on a Flutter screen — the Semantics widget, screen-reader labels (TalkBack/VoiceOver), colour contrast, minimum tap-target size, system font scaling, or semantics in a web build. Also triggers on "make this accessible", "a11y", or a request for an accessibility review.
---

You are implementing or auditing accessibility (a11y) for a Flutter app.

## Context

Accessibility pressure differs by surface, and it changes what you prioritise:

- **Customer-facing web** — the highest bar. Screen-reader semantics are off by
  default on Flutter web (see section 5), the audience is unknown, and in many
  jurisdictions this is a legal requirement, not a nicety.
- **Internal/admin web** — keyboard navigation and contrast matter most; staff
  use these all day.
- **Mobile** — platform screen readers (TalkBack/VoiceOver) mostly work if you
  give widgets labels and keep tap targets large enough.

Choose based on the skill argument, if one was given, or on the screen/task at hand:
- `audit` — Review existing code for a11y violations and produce a report with fixes
- `implement` — Add accessibility annotations and fixes to existing widgets
- `web-setup` — Enable Flutter web semantics and configure for screen readers
- If no mode specified, default to `implement`

---

## 1. Semantic Annotations

Wrap custom widgets with `Semantics` to expose meaning to assistive technology:

```dart
// Label interactive elements
Semantics(
  label: 'Adicionar ao pedido',
  button: true,
  child: CustomAddButton(...),
)

// Group related elements into a single announcement
MergeSemantics(
  child: Row(
    children: [
      Icon(Icons.star),
      Text('4.5'),
    ],
  ),
)

// Hide decorative elements from screen readers
ExcludeSemantics(
  child: DecorativeBackground(),
)
```

### When to use each

| Widget | Use case |
|---|---|
| `Semantics(label:)` | Custom widgets that lack built-in semantics (raw GestureDetector, Container with onTap) |
| `Semantics(button: true)` | Custom buttons not using Material `ElevatedButton`/`TextButton` |
| `Semantics(header: true)` | Section headers (category names in a catalog) |
| `Semantics(image: true, label:)` | Product/content images |
| `MergeSemantics` | Icon + text pairs, price + currency pairs |
| `ExcludeSemantics` | Purely decorative images, background gradients, dividers |

Material widgets (`ElevatedButton`, `TextField`, `Checkbox`) already have built-in semantics. Do NOT double-wrap them unless overriding the default label.

---

## 2. Color Contrast

Follow WCAG 2.1 minimum contrast ratios:

| Element | Minimum ratio |
|---|---|
| Normal text (< 18sp) | **4.5:1** |
| Large text (>= 18sp or >= 14sp bold) | **3.0:1** |
| UI components & graphical objects | **3.0:1** |

### Verification

- Use the **Accessibility Scanner** on Android (Settings > Accessibility)
- Run **Lighthouse** audit in Chrome DevTools for the web build
- Use online contrast checkers (WebAIM Contrast Checker) with hex values from `ds_colors.dart`

### Violations that show up again and again

- Light gray text on white backgrounds for secondary info (descriptions, prices in light theme)
- Disabled button text that falls below 3.0:1
- Placeholder text in search fields

When fixing, update colors in `design_system/` tokens so the fix propagates everywhere.

---

## 3. Tap Targets

All interactive elements must be at least **48x48 logical pixels**:

```dart
// BAD - icon button too small
GestureDetector(
  onTap: onAdd,
  child: Icon(Icons.add, size: 24),
)

// GOOD - ensure minimum hit area
IconButton(
  onPressed: onAdd,
  icon: Icon(Icons.add, size: 24),
  // IconButton already enforces 48x48 by default
)

// GOOD - for custom widgets, use constraints
GestureDetector(
  onTap: onAdd,
  child: ConstrainedBox(
    constraints: const BoxConstraints(minWidth: 48, minHeight: 48),
    child: Center(child: Icon(Icons.add, size: 24)),
  ),
)
```

### Spacing between targets

Adjacent tap targets must have enough spacing to avoid accidental taps. Minimum **8px** gap between interactive elements.

---

## 4. Screen Reader Support

### Labels for images (product photos, avatars, thumbnails)

```dart
// Product image in a catalog
Semantics(
  image: true,
  label: 'Foto do produto: ${product.name}',
  child: CachedNetworkImage(
    imageUrl: product.imageUrl,
    // ...
  ),
)

// Decorative images (backgrounds, patterns)
ExcludeSemantics(
  child: Image.asset('assets/background_pattern.png'),
)
```

### Labels for icon buttons

```dart
IconButton(
  icon: Icon(Icons.shopping_cart),
  tooltip: 'Ver carrinho', // tooltip doubles as semantic label
  onPressed: () => openCart(),
)
```

### Price announcements

```dart
// Announce price clearly
MergeSemantics(
  child: Row(
    children: [
      Text('R\$'),
      Text('29,90'),
    ],
  ),
)
// Screen reader announces: "R$ 29,90"
```

---

## 5. Web Accessibility (any web build)

Flutter web **disables semantics by default** for performance. For customer-facing apps this is unacceptable.

### Enable semantics in web builds

In the app's `web/index.html`, add before the Flutter loader script:

```html
<script>
  // Enable accessibility for Flutter web
  window.flutterConfiguration = {
    canvasKitBaseUrl: '/canvaskit/',
  };
</script>
```

In the app's `main.dart`:

```dart
import 'package:flutter/semantics.dart';

void main() {
  // Force enable semantics for web accessibility
  WidgetsFlutterBinding.ensureInitialized();
  SemanticsBinding.instance.ensureSemantics();

  runApp(const MyApp());
}
```

### Alternative: enable via URL parameter

For testing, append `?semantics=true` to the web URL to activate the semantics tree without code changes.

### Web-specific considerations

- Ensure the HTML `<title>` tag is descriptive (e.g., "Product Catalog — Acme")
- Add `lang="pt-BR"` to the `<html>` tag in `index.html`
- Flutter web renders to canvas, so ALL accessibility depends on the Semantics tree being correct

---

## 6. Font Scaling

Support dynamic text sizing for users who increase system font size:

```dart
// BAD - breaks with large fonts
SizedBox(
  height: 48,
  child: Text('Categoria', style: TextStyle(fontSize: 16)),
)

// GOOD - adapt to text scale
ConstrainedBox(
  constraints: const BoxConstraints(minHeight: 48),
  child: Text('Categoria', style: TextStyle(fontSize: 16)),
)
```

### Guidelines

- Use `minHeight` / `minWidth` instead of fixed `height` / `width` for containers holding text
- Test with system font scale at **2.0x** (Settings > Display > Font size)
- Ensure text does not overflow or get clipped — use `Flexible`, `Expanded`, or `overflow: TextOverflow.ellipsis` with `maxLines`
- Product names and descriptions must remain readable at large scales

### Limiting scale when necessary

Only limit scale factor for truly constrained UI (e.g., small price badges):

```dart
MediaQuery.withClampedTextScaling(
  minScaleFactor: 1.0,
  maxScaleFactor: 1.5,
  child: PriceBadge(...),
)
```

---

## 7. Focus Management (Web)

Keyboard navigation is essential for web accessibility:

```dart
// Ensure custom interactive widgets can receive focus
Focus(
  child: GestureDetector(
    onTap: onTap,
    child: MenuItemCard(...),
  ),
)

// Set logical focus traversal order
FocusTraversalGroup(
  policy: OrderedTraversalPolicy(),
  child: Column(
    children: [
      FocusTraversalOrder(
        order: NumericFocusOrder(1),
        child: SearchBar(),
      ),
      FocusTraversalOrder(
        order: NumericFocusOrder(2),
        child: CategoryFilter(),
      ),
      FocusTraversalOrder(
        order: NumericFocusOrder(3),
        child: ProductGrid(),
      ),
    ],
  ),
)
```

### Keyboard shortcuts for the web build

- **Tab** — move between interactive elements
- **Enter/Space** — activate buttons
- **Escape** — close modals/bottom sheets

Ensure all modals trap focus (Material `showDialog` and `showModalBottomSheet` do this automatically).

---

## 8. Accessible Images

Every meaningful image needs descriptive text:

```dart
// Product image
Semantics(
  image: true,
  label: product.name, // "Pizza Margherita"
  child: ProductImage(url: product.imageUrl),
)

// Category icon
Semantics(
  image: true,
  label: 'Categoria: ${category.name}',
  child: CategoryIcon(icon: category.icon),
)

// Restaurant logo
Semantics(
  image: true,
  label: 'Logo do restaurante ${restaurant.name}',
  child: RestaurantLogo(url: restaurant.logoUrl),
)

// Decorative divider — exclude
ExcludeSemantics(
  child: Image.asset('assets/divider_ornament.png'),
)
```

---

## 9. Accessible Forms

Forms (search, checkout, sign-up) must announce labels and errors:

```dart
TextFormField(
  decoration: InputDecoration(
    labelText: 'Nome completo', // announced by screen reader
    hintText: 'Digite seu nome',
    errorText: hasError ? 'Nome e obrigatorio' : null, // announced on error
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Nome e obrigatorio'; // announced when validation fails
    }
    return null;
  },
)
```

### Guidelines

- Always use `labelText` on `InputDecoration`, not just `hintText` (hints disappear on focus)
- Error messages must be descriptive: say what is wrong, not just "campo invalido"
- After form submission with errors, move focus to the first field with an error
- Group related fields with `MergeSemantics` or `Semantics(label:)` when context is needed

---

## 10. Audit Workflow

### When mode is `audit`

1. **Scan widgets** for missing `Semantics` on:
   - Images (`Image`, `CachedNetworkImage`, `SvgPicture`)
   - Custom buttons (`GestureDetector`, `InkWell` without `Semantics`)
   - Icon-only buttons without `tooltip`
   - Decorative elements that should use `ExcludeSemantics`

2. **Check tap target sizes** — find interactive widgets with constrained sizes below 48x48

3. **Review color usage** — cross-reference text colors against background colors in the design system tokens

4. **Check web semantics** — verify `SemanticsBinding.instance.ensureSemantics()` is called in every web build

5. **Check font scaling** — find fixed `height`/`width` on containers holding text

6. **Produce report** in this format:

```
## Accessibility Audit: [app_name]

### Critical (must fix)
- [ ] Issue description — file:line

### Important (should fix)
- [ ] Issue description — file:line

### Minor (nice to have)
- [ ] Issue description — file:line
```

### Manual testing checklist

| Platform | Tool | How to test |
|---|---|---|
| Android | TalkBack | Settings > Accessibility > TalkBack. Navigate the app with swipe gestures. |
| iOS | VoiceOver | Settings > Accessibility > VoiceOver. Swipe to navigate. |
| macOS | VoiceOver | Cmd+F5. Use arrow keys to navigate web apps in Safari/Chrome. |
| Web | Lighthouse | Chrome DevTools > Lighthouse > Accessibility. Target score >= 90. |
| Web | axe DevTools | Chrome extension. Finds WCAG violations in rendered page. |
| Web | Tab navigation | Press Tab through the entire checkout/browse flow without using a mouse. |

### Lighthouse audit for the web build

```bash
# Run Lighthouse CI from command line
npx lighthouse http://localhost:PORT --only-categories=accessibility --output=json --output-path=./a11y-report.json
```

Target: **Lighthouse accessibility score >= 90** for the web build.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.