I am a big proponent of component-driven development, and don’t agree with the utility class method of styling. In component-driven development, you offer blackbox abstractions of common implementations. Meanwhile, the utility class method allows for more customization. In a systematic approach, we want our experiences to be consistent and therefore follow the same patterns and conventions. Distributing these blackboxes that align with guidelines by default makes a consistent experience easy to create and manage. Whereas, trying to learn and follow guidelines from other resources to implement piecemeal comes with misintereptations and other inconsistencies.
However, in this work there has been one concept that I’ve stuggled to put into this component-driven ecosystem; screenreader only as it has traditionally existed as a class (eg., .sr-only
) added to an otherwise benign element.
Amongst the library
The purpose of screenreader only is to mark a section of an interface that is meant specifically for assistive technologies to pick up within the experience; invisible to sighted users. Here’s a common example:
<button type="button">
<svg
focusable="false"
aria-hidden="true"
viewBox="0 0 24 24">
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5
6.41 10.59 12 5 17.59 6.41 19 12
13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
<span class="sr-only">Close</span>
</button>
In this example, sighted users will be able to see the path of the <svg/>
in the shape of an ‘x’ with no other visual information. Non-sighted users who leverage assistive technology should receive “Close” from their tool and ignore the data set by the <svg/>
. There’s more about this composition and words about screenreader only versus aria-label
in this post by Chris Ferdinandi.
My gut reaction is that it would be odd to offer such a component that simply applies the styles required to make it hidden and for it to be offered as an exception to an otherwise utility-free ecosystem. However, I don’t like exceptions, I strive for consistent thoughtful systems.
My first thought is where a <ScreenreaderOnly/>
would live amongst other components, if it is in fact a component itself. This is assuming that the library is vast and requires subsections that aim to direct visitors to the appropriate component through navigation. First I’d assume this might live with other “accessibility” components. However, this also assumes that folks are actively looking to include accessibility in their experiences; something that we can only dream of across an organization.
Further, we might consider accessibility as a quality of components, perhaps not a component itself. As I’m having difficulty thinking about other possible memebers of this family that would also be associated with the <ScreenreaderOnly/>
component.
My next thought is that because it is a quality, perhaps it isn’t a component but a property on an existing component we set. Because we expect that the content of this conceptual <ScreenreaderOnly/>
component to only ever be text, it might be safe to consider that it is a property of a text component.
<Text screenreaderOnly>To begin, start by...<Text/>
The <Text/>
component will be widely more popular than the possible <ScreenreaderOnly/>
component, which will cause more folks to potentially visit its API and learn about this helpful feature. This being an opportunity to educate when its use is appropriate.
When is it appropriate
I did quite a bit of reading and research before writing this post. As I’m an advocate for accessibility, I don’t consider myself an expert and definitely have areas of improvement in my practice. I wanted to know when the concept of screenreader only was appropriate if at all.
For this topic, I highly recommend reading Scott O’Hara’s post called Inclusively Hidden.
In Scott’s post, he describes some alternatives to screenreader only; specifically one loophole in particular.
By attaching an
aria-describedby
oraria-labelledby
attribute to a focusable element, and setting the ARIA attribute’s value to the completely hidden element’s id, screen readers will announce the content of the completely hidden element.
Here’s his example:
<p class="visually-hidden" id="example_desc">
Here are specific instructions for the type
of information this form input is expecting to receive...
</p>
<label for="example">
Example
</label>
<input type="text" id="example" aria-describedby="example_desc">
In this way, we still need to apply styles to the .visually-hidden
element in order for it to disappear from view. But this also demonstrates how to connect that content to an interactive element for further context by using aria-describedby
.
For completeness, here’s what Scott recommends as the styles for visually-hidden
:
.visually-hidden:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
I’m now imagining some sort of interface for when screenreaderOnly
is used on the <Text/>
component to ensure it is connected to something.
// Source
function Text({ screenreaderOnly, children }) {
/**
* We wouldn't want to always add an id to all text
* instead we'd add conditionally for screenreaderOnly.
* This is just for this example.
*/
const id = React.useId();
React.useEffect(() => {
/**
* Consider appending the id instead of replacing
* `aria-describedby` can accept space separated ids
*/
screenreaderOnly
?.current
?.setAttribute('aria-describedby', id);
}, [screenreaderOnly, id]);
/**
* Assume additional logic to
* bring-your-own HTML tag (BYOHTMLT)
* and force text node children.
*/
return (
<div
className={ screenreaderOnly && 'visually-hidden' }
id={ id }>{ children }</div>
);
}
// Usage
function MyForm() {
const ref = useRef();
return (
<Text screenreaderOnly={ ref }>
Here are specific instructions for
the type of information this form
input is expecting to receive...
</Text>
<label for="example">
Example
</label>
<input type="text" id="example" ref={ ref }>
)
}
However you feel about using a useEffect
to then use a DOM API to set an attribute that this component shouldn’t have control over is less important than the concept of connecting these two elements for accessibility purposes in an easy to use interface. It would be important to me that using <Text screenreaderOnly>
requires the connection be maintained. It is too easy to accidentally rename id
s in a vast composition of components causing accessibility wiring to break.
Also you can have your own naming convention to denote that the expected input is a ref
to the element that you wish to describe.
It’s important to know that aria-label
, aria-labelledby
and aria-describedby
are not valid on all elements. The general gist is that these are meant for interactive elements or landmark elements. This interface above could be enhanced to warn if the ref
is attached to an element that is inappropriate but would need some more conditional logic to identify this.
Why aria-describedby
The benefits of using aria-describedby
for screenreader only content is that first it doesn’t replace existing labeling (as aria-labelledby
would); it is meant to accompany it. It also benefits by auto-translation technologies that may struggle finding aria-label
content will more easily find aria-describedby
content because it is written within a discoverable DOM node.
Certainly aria-describedby
isn’t always going to be the correct solution. I believe if you’ve done due dilligence in crafting a well prepared experience, with semantic elements and appropriate attribute usage, this approach is probably going to help provide additional context to visually-impared users for complex interfaces.
Know that if the content would also help visual users, you should absolutely consider putting the content inline with the composition and forego additional configuration complexity.
And as always, reconsider the complexity of your experience overall, as you might be able to avoid cumbersome instructions for something simpler.