Splitting
Splitting copied to clipboard
Accessibility
Currently, some advanced effects for Splitting require using psuedo elements with the data-word
and data-char
attributes populating them. Unfortunately, screen readers read the psuedo elements populated with content: attr(data-word)
, effectively repeating every word.
For example: https://codepen.io/shshaw/pen/LBXZwW
In experimenting with VoiceOver, the best approach I found was to add an aria-label
to the parent element. This still causes issues when they focus inside the element, but it's a much better experience on the main heading.
<h1 data-splitting aria-label="My Heading" ...>
<span class="word" aria-role="presentation">...
Proposed solution:
- Add an
accessibility
option that'strue
by default. - When
true
:- Add
aria-label
of theel.innerText
when splitting text, if noaria-label
already present. - Created elements (
<span>
s ) getrole="presentation"
.
- Add
- Users can turn it off with
accessibility: false
if they want to handle accessibility on their own
We also should add this to the psuedo element CSS for browsers that do support:
speak: never;
speak: none;
user-select: none;
pointer-events: none;
Any other considerations?
Hey,
What about using speech
, aural
and maybe reader
at-media rules to hide the visibility of those pseudo-elements?
@media speech, aural, reader {
[data-char]:after,
[data-word]:after {
display: none;
visibility: hidden;
}
}
Never tested yet :)
I had tried that already and unfortunately did not make any difference for macOS Chrome & VoiceOver. Any idea what browsers / voice-to-text programs support it?
Experimenting with VoiceOver, thanks to some guidance from https://axesslab.com/text-splitting/
https://codepen.io/shshaw/pen/YBeqRO
Splitting().forEach( s => {
// Causes the entire block to be read as a whole with no distinction of headings
// s.el.setAttribute('role', 'text');
// Causes separation of the blocks, but loses some of the semantics between headings
// [...s.el.childNodes].forEach( c => c.setAttribute('role', 'text') );
// Works pretty well, but causes words to be read individually in paragraphs/inline elements
s.words.forEach( word => {
word.setAttribute('role', 'text');
});
// Might help prevent VoiceOver from treating the chars as individual elements to read.
s.chars.forEach( char => {
char.style.setProperty('--random', Math.random());
char.setAttribute('role', 'presentation');
});
});
Could be worth copying the original DOM content somewhere hidden, and utilizing aria-labelledby
.
What about copying the textContent to an aria-label when doing a character split rather than using labeledby?
Yeah; that's mentioned in the original post. The main issue is that loses some of the semantics if you had a list, headings, etc. within the main split component, and it would be a massive attribute in the DOM for paragraphs / long content.
That is what happens when I try to respond to a 6 month+ thread without re-reading the whole thing :smile:
I am not opposed to keeping a copy of the original in the DOM for labeledby. How do you think we should handle re-splitting with that in mind? I wonder if it would get complicated...
So glad I found this — was wondering how Splitting.js handles accessibility (and search indexing, which is a separate but related issue).
I'm curious about using Splitting to create custom-styled headings on a site, and it seems like for this use case the s.el.setAttribute('role', 'text');
example would definitely suffice — that is, it'll read the whole heading as one block element, vs. word-by-word. However, if I were to use it on multiple elements (like heading + body), I'd probably want to use [...s.el.childNodes].forEach( c => c.setAttribute('role', 'text') );
so that it breaks apart these elements.
Am I understanding that correctly?
@thejohnnyhill No official recommendation yet. role="text"
is a little heavy handed and reduces accessibility for headings, lists and other elements with semantic meanings. For now, this approach works well, but isn't ideal.
// If the split element is only text, do this:
s.el.setAttribute('aria-label', s.el.textContent);
// If element has actual child elements ( `<div data-splitting> <h1>...</h1> <p>...</p>` etc ), do this:
[...s.el.childNodes].forEach( c => c.setAttribute('aria-label', c.textContent) );
// Prevents chars from appearing as individual elements, but is also preventing selecting text.
s.chars.forEach( char => {
char.setAttribute('aria-hidden', true);
});
Still working through some approaches and seeking feedback on the best way to make screenreaders ignore these decorative elements since aria-role="presentation"
doesn't seem to do it properly.
Some experimentations: https://codepen.io/shshaw/pen/YBjjyN?editors=1010
Here's how Lettering.js handles the issue: https://github.com/davatron5000/Lettering.js/pull/51
Basically like recommended above.
For automating this as a default option, we'll need to figure out a good way to apply the aria labels to non-Splitting created childNodes/parents in a sensible way.
Another reference: https://github.com/ZeroSpree/CSSans.Pro#quick-start https://github.com/ZeroSpree/CSSans.Pro#accessibility
@thejohnnyhill role="text"
isn't appropriate, because:
- The
role
attribute overrides the native semantics of the element it is applied to. If splitting.js appliesrole="text"
as a generic approach, it will be very damaging to the author's intended semantics. As a basic example,<h1 role="text">Main heading</h1>
would not be reported as a heading to assistive technology. Screen reader users would not be able to use their heading navigation tools to find it. - The
text
role is a non-standard role. It isn't included in the ARIA 1.0 or 1.1 recommendations, and it isn't in the current ARIA 1.2 working draft (18 Decemberr 2018) or editorial draft. AFAIK WebKit is the only engine which implements this role. OS-level accessibility APIs do have a text role, and the idea of a corresponding ARIA role has been discussed for a long time. However it hasn't achieved a consensus, and there are discussions about whether it should only be permitted on restricted set of native elements. At this stage, I wouldn't recommend it for use by any library code.
@shshaw
Still working through some approaches and seeking feedback on the best way to make screenreaders ignore these decorative elements since aria-role="presentation" doesn't seem to do it properly.
You've misunderstood the meaning of role="presentation"
. It's purpose is to remove native semantics, but not remove content, from the accessibility tree. Child nodes will in the DOM will still be included in the accessibility tree with their roles unaffected. Many developers have been confused by this, so ARIA 1.1 introduced role="none"
as a synonym for role="presentation"
.
If you want a generated character <span>
to be ignored by assistive technology, use the aria-hidden="true"
property.
Using aria-hidden="true"
on the generated spans is a good idea, so long as the text is presented by another method.
Using aria-label
on the elements being split isn't going to be reliable in all cases; it may be inneffective on some element types.
- For guidance see:
- It will work on heading elements. Indeed, the Using ARIA guide warns against using
aria-label
on headings precisely because it will override the element content. In the case here, that's actually the hack we want. - It will work on links.
- It may be completely ineffective on
<div>
,<p>
, and many others.
I'm not sure this can ever be solved in JS library code alone. Rather, it may be preferable to provide accessibility guidance in documentation. It should ideally be a prominent section in the docs, like a top-level heading/chapter.
Idea: many use-cases for splitting.js are about providing a fancy graphical effect for text, such as logotypes. In some cases it may be reasonable to use an image role, e.g. <div role="img" aria-label="ACME">ACME</div>
, which will convey the label to assistive tech. This is semantically equivalent to using an <img>
for images of text, which is a well supported and widespread approach for fancy text/logos. However this is clearly a decision for authors, and we can't assume that splitting.js is always being used for fancy graphical effects. This is the sort of usage that can be covered in accessibility documentation, but could be dangerous to assume in library code.
An alternative approach to aria-label
is to use a visually-hidden span as the accessible text. This is quite a robust approach to providing accessible text alternatives.
e.g.
<h1>ACME</h1>
is transformed to
<h1>
<span class="visually-hidden">ACME</span>
<span aria-hidden="true">A</span>
<span aria-hidden="true">C</span>
<span aria-hidden="true">M</span>
<span aria-hidden="true">E</span>
</h1>
PRO:
- Doesn't alter the semantics of the target element, like
role="text"
orrole="img"
would. - Works equally well for different element types (
h1
,p
,div
,a
,blockquote
), unlikearia-label
.
CON:
- Assumes a visually hidden CSS utility class is available. These have different names across various CSS libraries:
visually-hidden
,visuallyhidden
,sr-only
, etc.. Perhaps this class name could be a configurable option? - When selecting text to cut/copy, it's likely this will catch the visually hidden span AND the individual character spans. So when you paste it somewhere else, you'll have something like
ACME A C M E
. Whilst annoying, this is a much lesser concern than presenting an accessible version of the text for the main use-case of reading the text.