<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Meredith Boyce: writing</title>
  <subtitle>Notes on accessibility engineering</subtitle>
  <link href="https://meredithb104.github.io/feed.xml" rel="self" />
  <link href="https://meredithb104.github.io/posts/" />
  <id>https://meredithb104.github.io/posts/</id>
  <updated>2026-09-26T20:17:04.363Z</updated>
  <author><name>Meredith Boyce</name></author>
  <entry>
    <title>TalkOver, revisited</title>
    <link href="https://meredithb104.github.io/posts/talkover-revisited/" />
    <id>https://meredithb104.github.io/posts/talkover-revisited/</id>
    <updated>2026-09-17T00:00:00Z</updated>
    <published>2026-09-17T00:00:00Z</published>
    <summary>Years ago, I wanted to rewrite TalkBack to behave like VoiceOver. I finally read the TalkBack source. Most of the &quot;rewrite&quot; already ships as a settings screen.</summary>
    <content type="html">&lt;p&gt;Once upon a time, long before the age of AI, I proposed rewriting Android&amp;#39;s TalkBack code so that it would work more like VoiceOver on Apple devices. My grand invention was to be called &amp;quot;TalkOver.&amp;quot;&lt;/p&gt;
&lt;p&gt;I never got past the name. The idea stayed with me long enough that I finally asked the question properly: how far apart are these two gesture models? Would TalkOver have needed a rewrite, or was the gap smaller than it looked from the outside?&lt;/p&gt;
&lt;p&gt;I did not ask a forum. Google has published TalkBack&amp;#39;s source, so I read it. Two files answer the question. &lt;code&gt;gesture_preferences.xml&lt;/code&gt; defines every gesture slot TalkBack recognizes, from a one-finger swipe to a four-finger triple-tap. &lt;code&gt;donottranslate.xml&lt;/code&gt; names more than a hundred actions you can bind to any slot, and it records the default binding for each one. That is ground truth, not a summary of ground truth.&lt;/p&gt;
&lt;h2 id=&quot;the-gap-is-smaller-than-talkover-assumed&quot;&gt;The gap is smaller than TalkOver assumed&lt;/h2&gt;
&lt;p&gt;Several of VoiceOver&amp;#39;s core gestures are already TalkBack&amp;#39;s defaults under different names.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; role=&quot;region&quot; aria-label=&quot;Table: The gap is smaller than TalkOver assumed. Scrolls sideways on narrow screens.&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope=&quot;col&quot;&gt;VoiceOver&lt;/th&gt;
&lt;th scope=&quot;col&quot;&gt;TalkBack default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Swipe right or left moves to the next or previous item&lt;/td&gt;
&lt;td&gt;Identical: &lt;code&gt;NEXT&lt;/code&gt; and &lt;code&gt;PREVIOUS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swipe up or down adjusts the rotor&amp;#39;s current setting&lt;/td&gt;
&lt;td&gt;Same idea: &lt;code&gt;SELECTED_SETTING_NEXT_ACTION&lt;/code&gt; and &lt;code&gt;SELECTED_SETTING_PREVIOUS_ACTION&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-finger tap pauses or resumes speech&lt;/td&gt;
&lt;td&gt;Identical: &lt;code&gt;PAUSE_OR_RESUME_FEEDBACK&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-finger double-tap is the &amp;quot;magic tap&amp;quot; (play, pause, or answer a call)&lt;/td&gt;
&lt;td&gt;Identical intent: &lt;code&gt;MEDIA_CONTROL_OR_VOICE_INPUT&lt;/code&gt; on the same gesture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The younger engineer who named TalkOver would have rewritten four gestures that needed no rewriting.&lt;/p&gt;
&lt;h2 id=&quot;where-they-diverge-and-what-closes-the-gap&quot;&gt;Where they diverge, and what closes the gap&lt;/h2&gt;
&lt;p&gt;The real differences are narrower than &amp;quot;these are different apps,&amp;quot; and most of them are one settings screen away from closing.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; role=&quot;region&quot; aria-label=&quot;Table: Where they diverge, and what closes the gap. Scrolls sideways on narrow screens.&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope=&quot;col&quot;&gt;VoiceOver&lt;/th&gt;
&lt;th scope=&quot;col&quot;&gt;TalkBack default&lt;/th&gt;
&lt;th scope=&quot;col&quot;&gt;What closes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Three-finger swipe up or down scrolls the page&lt;/td&gt;
&lt;td&gt;Bound to setting selection; scrolling lives on two-finger swipes&lt;/td&gt;
&lt;td&gt;Rebind the three-finger swipes to &lt;code&gt;SCROLL_UP&lt;/code&gt; and &lt;code&gt;SCROLL_DOWN&lt;/code&gt;. Both are assignable actions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-finger swipe up reads the whole screen from the top&lt;/td&gt;
&lt;td&gt;Scrolls up&lt;/td&gt;
&lt;td&gt;&lt;code&gt;READ_FROM_TOP&lt;/code&gt; exists as an action. It is not on that gesture by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-finger swipe down reads from the current position&lt;/td&gt;
&lt;td&gt;Scrolls down&lt;/td&gt;
&lt;td&gt;&lt;code&gt;READ_FROM_CURRENT&lt;/code&gt; already sits on the two-finger triple-tap. Bind it to the swipe as well if you want the VoiceOver gesture, not only the result.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Four-finger tap at the top or bottom jumps to the first or last item&lt;/td&gt;
&lt;td&gt;Unbound&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FIRST_IN_SCREEN&lt;/code&gt; and &lt;code&gt;LAST_IN_SCREEN&lt;/code&gt; exist and sit idle. The four-finger triple-tap slot is unassigned and waiting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-finger swipe left or right moves a page&lt;/td&gt;
&lt;td&gt;Android has no general page concept&lt;/td&gt;
&lt;td&gt;The closest fit is the four-finger swipe left or right, which is already &lt;code&gt;PREV_CONTAINER&lt;/code&gt; and &lt;code&gt;NEXT_CONTAINER&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Then there is the one gap no settings screen closes. VoiceOver&amp;#39;s two-finger scrub, a &amp;quot;z&amp;quot; drawn on the glass, goes back or dismisses whatever is open. TalkBack has no scrub gesture type at all. Its back gesture is a one-finger swipe down and then left, an angle swipe. The intent is the same, the shape is different, and there is no remap for that, because the shape does not exist on the Android side.&lt;/p&gt;
&lt;h2 id=&quot;what-i-would-tell-that-younger-engineer&quot;&gt;What I would tell that younger engineer&lt;/h2&gt;
&lt;p&gt;TalkBack did not need a rewrite. It needed someone to open Settings, Accessibility, TalkBack, Customize gestures, and treat it as the fully remappable surface it already is. TalkOver, as I imagined it, mostly ships today. You only have to know which handful of gestures to move.&lt;/p&gt;
&lt;p&gt;The one thing it cannot do is invent a gesture type that Android&amp;#39;s touch model does not support. That part of the old idea was right, for reasons I could not have articulated at the time.&lt;/p&gt;
&lt;p&gt;Every binding in this post comes from &lt;code&gt;gesture_preferences.xml&lt;/code&gt; and &lt;code&gt;donottranslate.xml&lt;/code&gt; in the &lt;a href=&quot;https://github.com/google/talkback&quot;&gt;google/talkback repository&lt;/a&gt;. If Google changes a default, those two files will say so before any support article does.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Accessibility is a build error, not a review comment</title>
    <link href="https://meredithb104.github.io/posts/accessibility-is-a-build-error/" />
    <id>https://meredithb104.github.io/posts/accessibility-is-a-build-error/</id>
    <updated>2026-09-17T00:00:00Z</updated>
    <published>2026-09-17T00:00:00Z</published>
    <summary>I made this site refuse to compile when a color pair fails WCAG contrast. Here is why, and what it caught on day one.</summary>
    <content type="html">&lt;p&gt;I have written the same finding hundreds of times: insufficient contrast, secondary text, dark mode, 1.4.3, fails. I have written it for enterprise clients, for a state agency, and for freelance clients who paid me to find it. Every time, somebody fixed it, and every time, it came back a release later.&lt;/p&gt;
&lt;p&gt;I am done writing that finding. On this site, it cannot exist.&lt;/p&gt;
&lt;h2 id=&quot;the-rule&quot;&gt;The rule&lt;/h2&gt;
&lt;p&gt;Every color in my design tokens can declare which backgrounds it sits on and what ratio it owes against each one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;textMuted&amp;quot;: {
  &amp;quot;value&amp;quot;: &amp;quot;#4A5561&amp;quot;,
  &amp;quot;$contrastAgainst&amp;quot;: [&amp;quot;bg&amp;quot;, &amp;quot;surface&amp;quot;, &amp;quot;bgSubtle&amp;quot;],
  &amp;quot;$minRatio&amp;quot;: 4.5
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the tokens compile, a script checks every pair with the WCAG 2.x formula. If any pair falls short, the script prints the pair, the ratio it got, and the ratio it needed, and then it exits. No CSS is written, and the site does not build. There are eighty-one pairs across three themes, checked on every deploy.&lt;/p&gt;
&lt;h2 id=&quot;what-it-caught-on-day-one&quot;&gt;What it caught on day one&lt;/h2&gt;
&lt;p&gt;I required the focus ring to reach 3:1 against both the page and the primary button. The build stopped me: 1.55:1 against the button in the light theme.&lt;/p&gt;
&lt;p&gt;The build was right. The ring sits two pixels outside the control, so the color next to it is the page, not the button. I had written a requirement that does not exist in WCAG, and the build would not let me ship it. Nobody reviewed that mistake. Nobody had to.&lt;/p&gt;
&lt;h2 id=&quot;why-tokens-and-not-a-linter&quot;&gt;Why tokens and not a linter&lt;/h2&gt;
&lt;p&gt;A linter looks at a rendered page. It cannot see the theme that is not active, the state nobody triggered, or the component that is absent from the page it is checking. I know this because I run linters for a living, and then I test by hand, because linters miss things.&lt;/p&gt;
&lt;p&gt;A token file is the one place where every color and every background exist together before any of them is drawn. Check there, and you have checked all of it, in milliseconds, before a human looks.&lt;/p&gt;
&lt;p&gt;I apply the same rule to anything a machine can decide. On this site, a post with an image and no alt text does not build. A post whose headings skip a level does not build. If a machine can decide it, the machine decides it first.&lt;/p&gt;
&lt;h2 id=&quot;what-is-left-for-me&quot;&gt;What is left for me&lt;/h2&gt;
&lt;p&gt;The audit still has work in it: focus that lands on the body after a dialog closes, a live region that mounts with its message already inside and never speaks, a carousel that moves because nobody stopped it. Those need a person with a screen reader. I am that person.&lt;/p&gt;
&lt;p&gt;Contrast never needed me. It needed a build step.&lt;/p&gt;
</content>
  </entry>
</feed>
