惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
D
DataBreaches.Net
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
Netflix TechBlog - Medium
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog
B
Blog
U
Unit 42
有赞技术团队
有赞技术团队
博客园 - 聂微东
GbyAI
GbyAI
宝玉的分享
宝玉的分享
F
Full Disclosure
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
MyScale Blog
MyScale Blog
Jina AI
Jina AI
Martin Fowler
Martin Fowler
IT之家
IT之家
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Docker
P
Proofpoint News Feed
A
About on SuperTechFans
I
InfoQ
博客园 - 【当耐特】
C
Check Point Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
Y
Y Combinator Blog
Project Zero
Project Zero
WordPress大学
WordPress大学
小众软件
小众软件
AWS News Blog
AWS News Blog
博客园 - 司徒正美
T
The Exploit Database - CXSecurity.com
L
LINUX DO - 热门话题
I
Intezer
Engineering at Meta
Engineering at Meta
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks
T
Tenable Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes

Adrian Roselli

Link + Popover Navigation Focusgroup Tests ∪ of Target Audiences (Accessibility, SEO, AEO/GEO) headingoffset is Not the Document Outline Algorithm Maybe Don’t Rely on Google’s “Modern Web Guidance” WCAG3 Contrast as of April 2026 Accessibility Law of Headlines Your Browser Can Already Speak a Page Honoring Mobile OS Text Size Barriers from Links with ARIA
You Know What? Just Don’t Split Words into Letters
Adrian Roselli · 2026-02-06 · via Adrian Roselli

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.

NVDA 2025.3.2 with Firefox 147.0.2. The text is announced both before and after animation.
JAWS 2026.2512.50 with Chrome 144. The text is never announced.
Narrator Win 11 25H2 26200.7623 with Edge 144. Only the first letter of the text is ever announced.
VoiceOver macOS 26.2 with Safari 26.2. Only a sub-set of the letters is exposed, and those are read letter-by-letter. This video is more sloppy owing to not feeding the audio properly and an electrician poking holes in my walls.
Orca Ubuntu 25.04 with Firefox 146. The text is announced both before and after animation.

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.

SplitText in Screen Readers
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 into is 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.

Update: 10 February 2026

Jeffrey Yasskin says the W3C TAG was looking at a proposal for <canvas> to split a JavaScript string into glyphs to render them in the canvas, with the TAG asking for the proposal to squeeze those “letter” into the DOM. I say “was” because as of yesterday the TAG closed it as “unsatisfied.” Read the TAG’s (via Matt) reasoning for why. Also, Jeffrey added more context on Masto.

Update: 1 April 2026

I haven’t evaluated Pretext. I feel like readers of this post have enough detail now that they can try it on their own. There are a couple posts discussing larger challenges, however: