You Know What? Just Don’t Split Words into Letters
This is an unplanned part two for Barriers from Links with ARIA. The title reflects my exasperation because this isn’t new, I’ve simply failed to be explicit about it over the last decade or so.
In 2012 I vented about TypeButter using <kern style="letter-spacing: -0.01em;"> for each letter. In 2020 I noted the AWWWards site wrapped every letter in a <div> for animation, which screen readers presented letter by letter. In 2022, it was BeeLine Reader using <span>s to achieve gradients across a word.
In 2026 I am finally writing about it because GSAP has its SplitText plug-in asserting screen reader support that doesn’t stand up to use. I appreciate the embedded video explains how it should work, and even includes an unnamed screen reader demo, but GSAP assumes authors will use SplitText in a very specific way and fails to note potential problems. That’s a bummer because now I have to be the buzzkill.
GSAP’s Demo
This is the animation demo on the SplitText page (or go to the debug view if you want to test it):
See the Pen SplitText Demo by GSAP (@GreenSock) on CodePen.
I also made a fork, just in case GSAP changes the code later (which would mess with future testing).
The simplest way to test this is to fire up a screen reader and try navigating the page with whatever method you prefer. You may get different results than I did, but that’s the fun of testing! Though I did get confirmation from one, then another, then a third screen reader user.
If you press the “Characters” button, this is the HTML output of the first line of text (which is in its own <div>):
<div aria-hidden="true" style="position: relative; display: block; text-align: center;">
<div aria-hidden="true" style="position: relative; display: inline-block;">
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">B</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">r</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">e</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">a</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">k</div>
</div>
<div aria-hidden="true" style="position: relative; display: inline-block;">
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">a</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">p</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">a</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">r</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">t</div>
</div>
<div aria-hidden="true" style="position: relative; display: inline-block;">
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">H</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">T</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">M</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">L</div>
</div>
<div aria-hidden="true" style="position: relative; display: inline-block;">
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">t</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">e</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">x</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">t</div>
</div>
<div aria-hidden="true" style="position: relative; display: inline-block;">
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">i</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">n</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">t</div>
<div aria-hidden="true" style="position: relative; display: inline-block; translate: none; rotate: none; scale: none; opacity: 1; transform: translate(0px);">o</div>
</div>
</div>
Five words.
The ARIA Non-Solution
The demo uses a <div>, which in the HTML AAM maps to the generic role. The problem here is that the generic role does not allow itself to be named by the author — which means aria-label is prohibited on it.
The example code GSAP offers, however, shows headings instead (which allow aria-label). It makes no mention of the restrictions on which roles allow aria-label. An author might think nothing of using aria-label on a <div>. Kind of like the embedded GSAP demo.
When aria-label is used on an element that allows it, the GSAP page makes no mention of other risks. I’ve repeatedly said aria-label may not auto-translate for users. It also doesn’t talk about WCAG SC 2.5.3 Label in Name risks when aria-label is applied to any control.
Hiding Content
Kind of a nitpick, but the GSAP page links to a CSS-Tricks post that itself simply links to Scott’s Inclusively Hidden post. Scott’s post got an update in 2023. CSS-Tricks’ post did not. I consider that a disservice to both Scott and readers.
Videos
I hate making these videos. It takes so much time. But it’s evidence. Or proof. Or something.
I was going to make videos for VoiceOver iPadOS with Safari, TalkBack with Chrome, and TalkBack with Firefox, but I got tired. If you’ve been reading my blog long enough, then you should know the commands to give it a try.
Similarly, you can pop open the Braille viewers on the desktop and see how those perform.
Results
I’ve reduced these to a binary yes/no. I also added mobile results, for which I made no videos.
| Pairing | Works? |
|---|---|
| NVDA / Firefox | yes |
| JAWS / Chrome | no |
| Narrator / Edge | no |
| VO macOS / Safari | no |
| Orca / Firefox | no |
| TalkBack / Chrome | yes |
| TalkBack / Firefox | no |
| VO iPadOS / Safari | no |
Bug Report
This post is a warning to authors. I’ve also filed an issue with GSAP asking the SplitText page to clarify the risks and limitations: #642 Screen Readers do not expose SplitText
I don’t expect them to be able to jump on the bug report immediately, and they will almost definitely want to perform further testing. So until they can tackle it, I strongly recommend avoiding SplitText.
Wrap-up
If you need to split words into their constituent letters in order to adjust kerning, give them gradients, animate them, or whatever, well, no you don’t. Find another method.
If the GSAP people who guaranteed their approach works with screen readers got it wrong, it seems likely you will too. Unless you have all the screen reader, browser, platform, and TTS variations along with the screen reader navigation skills needed to perform ongoing and robust testing.
Which you don’t.
Update: Same Damn Day
Turn your terminal intois completed letter by letter first with
an interface designer,which is deleted letter by letter and replaced with
a frontend wizard,and then replaced by
an accessibility expert.The browser dev tools show it’s all in a
<span> with an aria-label.
The makers of Tailwind, dissatisfied with pushing verbose class names on human authors only, have decided to target LLMs with ui.sh (pronounced “wish”). While this hilarious, non-parody home page doesn’t make the mistake of wrapping every letter in its own element, it does make the mistake of using aria-label.
I’ve decided it warrants a mention here.
The good news is this will help guarantee the need for human accessibility practitioners over poorly-trained LLMs.
Leave a Comment or Response