Seems like we’re always talking about clipping text around here. All it takes is a little browsing to spot a bunch of things we’ve already explored.
It’s harder than it looks! And there’s oodles of consideration that go into it! Last time I visited this, I recreated a cool-looking implementation on MDN.
In there, I noted that VoiceOver respects the full text and announces it.
I started wondering: What if I or someone else wanted to read the full text but didn’t have the assistive tech for it? It’s almost a selfish-sounding thing because of long-term muscle memory telling me I’m not the user. But I think that’s a valid request for a more inclusive experience. All folks should be able to get the full content, visually or announced.
I didn’t want to make the same demo, so I opted for something a little different that poses similar challenges, perhaps even more so. This is it:
I know, not the loveliest thing ever. But it’s an interesting case study because:
This setup does what my MDN experiment doesn’t: give users without assistive tech a path to the full content. Right on, ship it! But wait…
Now VoiceOver (I’m sorry that’s all I’ve tested in) announces the button. I don’t want that. I don’t even need that because a screen reader already announces the full text. It’s not like someone who hears the text needs to expand the panel or anything. They should be able to skip it!
But should we really “hide” the button? So much conventional wisdom out there tells us that it’s terrible to hide and disable buttons. Any control that’s there for sighted readers should be present for hearing listeners as well.
If we were to simply drop disabled="true"
on the button, that prevents the screen reader from pressing the button to activate something needlessly. But now we’ve created a situation where we’ve disabled
something without so much as an explanation why. If I’m hearing that there’s a button on the page and it’s disabled
(or dimmed), I want to know why because it sounds like I might be missing out on something even if I’m not. Plus, I don’t want to disable
the button by default, especially for those who need it.
This is where “real world” Geoff would likely stop and question the pattern altogether. If something is getting this complicated, then there’s probably a straighter path I’m missing. But we’re all learners here, so I gave that other Geoff a shiny object and how he’s distracted for hours.
Let’s say we really do want to pursue this pattern and make it where the button remains in place but also gives assistive tech-ers some context. I know that the first rule of ARIA is “don’t use ARIA” but we’ve crossed that metaphorical line by deciding to use a <button>
. We’re not jamming functionality into a <div>
but are using a semantic element. Seems like the “right” place to reach for ARIA.
We could “hide” the button this way:
<!-- NOPE! -->
<button aria-hidden="true">Show Full Text</button>
Buuuut, slapping aria-hidden="true"
on a focusable element is not considered a best practice. There’s an aria-
approach that’s equivalent to the disabled
attribute, only it doesn’t actually disable
the button when we’re not using a screen reader.
<button aria-disabled="true">Show Full Text</button>
Cool, now it’s hidden! Ship it. Oh, wait again. Yes, we’ve aria-disabled
the button as far as assistive tech is concerned, but it still doesn’t say why.
Still some work to do. I recently learned a bunch about ARIA after watching Sara Soueidan’s “The Other C in CSS” presentation. I’m not saying I “get” it all now, but I wanted to practice and saw this demo as a good learning exercise. I learned a couple different ways we can “describe” the button accessibly:
- Using
aria-label
: If our element is interactive (which it is), we can use this to compose a custom description for the button, assuming the button’s accessible name is not enough (which it’s not). - Using
aria-labelledby
: It’s likearia-label
but allows us to reference another element on the page that’s used for the button’s description.
Let’s focus on that second one for a moment. That might look something like this:
<button aria-disabled="true" aria-labelledby="notice">Show Full Text</button>
<span id="notice">This button is disabled since assistive tech already announces the article content.</span>
The element we’re referencing has to have an id
. And since an ID can only be used once a page we’ve gotta make sure this is the only instance of it, so make it unique — more unique than my lazy example. Once it’s there, though, we want to hide it because sighted folks don’t need the additional context — it’s for certain people. We can use some sort of .visually-hidden
utility class to hide the text inclusively so that screen readers still see and announce it:
.visually-hidden:not(:focus):not(:active) {
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0); /* for IE only */
clip-path: inset(50%);
position: absolute;
white-space: nowrap;
}
Let’s make sure we’re referencing that in the CSS:
<button aria-disabled="true" aria-labelledby="notice">Show Full Text</button>
<span id="notice" class="visually-hidden">This button is disabled since assistive tech already announces the article content.</span>
This certainly does the trick! VoiceOver recognizes the button, calls it out, and reads the .visually-hidden
notice as the button’s description.
I’m pretty sure I would ship this. That said, the markup feels heavy with hidden span
. We had to intentionally create that span
purely to describe the button. It’s not like that element was already on the page and we decided to recycle it. We can get away with aria-label
instead without the extra baggage:
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quidem asperiores reprehenderit, dicta illum culpa facere qui ab dolorem suscipit praesentium nostrum delectus repellendus quas unde error numquam maxime cupiditate quaerat?
<button aria-disabled="true" aria-label="This button is disabled since assistive tech already announces the article content.">Read More</button>
VoiceOver seemed to read things a little smoother this time around. For example, it read the aria-label
content right away and announced the button and its state only once. Left to my own devices, I’d call this “baked” and, yes, finally ship it.
But not left to my own devices, this probably isn’t up to snuff. I inspected the button again in DevTools to see how the accessibility API translates it.
This looks off to me. I know that the button’s accessible name should come from the aria-label
if one is available, but I also know two other ARIA attributes are designed specifically for describing elements:
aria-description
: This is likely what we want! MDN describes it as “a string value that describes or annotates the current element.” Perfect for our purposes. But! MDN notes that this is still in the Editor’s Draft of the ARIA 1.3 specification. It shows up widely supported in Caniuse at the same time. It’s probably safe to use but I’m strangely conservative with features that haven’t been formally recommended and would be hesitant to ship this. I’m sure an actual accessibility practitioner would have a more informed opinion based on testing.aria-describedby
: We can leverage another element’s accessible description to describe the button just like we did witharia-labelledby
. Cool for sure, but not what we need right here since I wouldn’t want to introduce another element only to hide it for its description.
But don’t just take it from me! I’m feeling my way through this — and only partially as far as testing. This also might be a completely dumb use case — hiding text is usually a bad thing, including little excerpts like this. I’m expecting we’ll get some tips from smarter folks in the comments.
Here’s that final demo once again. It’s editable, so feel free to poke around.