<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SaHland</title>
    <description>The latest articles on DEV Community by SaHland (@sahland).</description>
    <link>https://dev.to/sahland</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4010911%2F5ee0e722-4b04-4f46-aa31-289b9bde1bf9.png</url>
      <title>DEV Community: SaHland</title>
      <link>https://dev.to/sahland</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sahland"/>
    <language>en</language>
    <item>
      <title>Lighthouse gives your Flutter app a perfect accessibility score. It's lying</title>
      <dc:creator>SaHland</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:58:23 +0000</pubDate>
      <link>https://dev.to/sahland/lighthouse-gives-your-flutter-app-a-perfect-accessibility-score-its-lying-51f2</link>
      <guid>https://dev.to/sahland/lighthouse-gives-your-flutter-app-a-perfect-accessibility-score-its-lying-51f2</guid>
      <description>&lt;p&gt;A few months ago I opened Lighthouse on a Flutter web build, ran the accessibility audit, and got a near-perfect score. Then I turned on a screen reader and tried to use the app. It was unusable. Buttons announced as "button" and nothing else. A form where the reader never said which field I was in. That gap — a green score next to a broken experience — is what sent me down this hole.&lt;/p&gt;

&lt;p&gt;Here's the thing nobody tells you when you pick Flutter: every accessibility tool you already know is blind to it. axe-core, Lighthouse, WAVE, Pa11y — they all read the DOM. Flutter doesn't have one. It paints everything to a single canvas, so to those tools your entire app is one anonymous &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element. They literally cannot see your buttons, your text, your contrast, anything.&lt;/p&gt;

&lt;p&gt;What does exist on pub.dev is useful but doesn't close the loop. &lt;code&gt;accessibility_tools&lt;/code&gt; gives you a debug overlay you click through by hand. A couple of scanners walk the tree once. None of them fail a pull request, none map a finding to the actual standard clause you'll be asked about, and none give you an artifact you can hand to a compliance person. So I built the thing I wanted: something that runs in a normal widget test, in CI, and tells me exactly what's wrong and where.&lt;/p&gt;

&lt;h2&gt;
  
  
  What using it looks like
&lt;/h2&gt;

&lt;p&gt;You add one call to a widget test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;testWidgets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Checkout screen is accessible'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tester&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tester&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pumpWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;CheckoutScreen&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tester&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;auditAccessibility&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passesAccessibilityGate&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line walks the live semantics tree, rasterizes the screen to check real contrast, re-pumps everything at 200% text size to catch overflow, and comes back with a report where every finding cites a WCAG success criterion and an EN 301 549 clause — pointed at the &lt;code&gt;file:line&lt;/code&gt; that produced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I pointed it at a random app to see if it actually works
&lt;/h2&gt;

&lt;p&gt;Fixtures prove nothing, so I grabbed an open-source Rick and Morty client off GitHub — a real app, BLoC, go_router, the usual — and wrote three lines of test against its widgets. First run, it flagged the favorite-star button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ [error] attest/interactive-name — WCAG 4.1.2 (A) / EN 301 549 §11.4.1.2
  Interactive element has no accessible name; a screen reader announces only its role.
  lib/uikit/widgets/buttons/character_favorite_button.dart:20
  Fix: wrap it in Semantics(label: …, button: true) or use IconButton(tooltip: …)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a genuine bug. The button is an &lt;code&gt;IconButton&lt;/code&gt; with an SVG star and no tooltip, so TalkBack reads it as "button" and the user has no idea it toggles a favorite. The report told me the WCAG criterion, the clause, and the line to fix — and it did not flag the app bar title next to it, which was fine. That last part matters more than the finding. A tool that cries wolf gets deleted after one afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;Twelve checks across three detection methods that source-level tools can't touch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Names and roles&lt;/strong&gt; — unlabeled buttons, images with no alt, form fields with no label, generic "Button" labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendered output&lt;/strong&gt; — real contrast measured from the actual pixels, and layout overflow when the system font goes to 200%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reachability&lt;/strong&gt; — touch targets that are too small, tap targets hidden from assistive tech.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that: a CI baseline gate that fails only on new findings (like coverage, by a coordinate-free fingerprint so a layout shift doesn't count as a regression), SARIF output for GitHub's Problems panel, and a screen-reader transcript — the literal sequence a screen reader would read out, in traversal order. I haven't seen another Flutter tool do that last one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'm being honest with you
&lt;/h2&gt;

&lt;p&gt;Automation catches maybe a third of accessibility problems. Nobody's automated tool catches "is this alt text actually meaningful" or "does this error message make sense" — those need a human. attest does not make you EAA-compliant and I don't claim it does. What it does is take the machine-checkable third off your plate and give you a structured checklist for the rest. Some of the heuristic checks can be noisy on unusual UIs; they're warnings, tagged as heuristic, and you can mute any rule in one line. It complements real TalkBack/VoiceOver testing. It doesn't replace it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why now
&lt;/h2&gt;

&lt;p&gt;The European Accessibility Act became enforceable on 28 June 2025. Within days, French disability groups sent formal legal notices to several large retailers over their websites and apps. Native mobile apps are explicitly in scope under EN 301 549 chapter 11. Penalties differ by country — Germany caps around €100k, Spain runs to €1M plus possible suspension of activity, Ireland even attaches criminal liability. Enforcement so far has gone after the web first because it's easier to test, with apps clearly next in line. Wiring a gate into CI while it's cheap beats finding out during an audit.&lt;/p&gt;

&lt;p&gt;It's early — &lt;code&gt;0.9.x&lt;/code&gt;, API still moving — and I'd genuinely like the feedback, especially false positives from real apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Package: &lt;a href="https://pub.dev/packages/attest_flutter" rel="noopener noreferrer"&gt;pub.dev/packages/attest_flutter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/sahland/attest" rel="noopener noreferrer"&gt;github.com/sahland/attest&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ship Flutter to the EU, spend ten minutes wiring one test into CI. Worst case you learn your app is fine.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>a11y</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
