Considering dynamic search results and content
Before we even begin, here are some key things I want you to take away from this post, which I had intended to write as a quick note of sorts… but I clearly don’t know how to to keep myself from exploding words everywhere.
Some quick takeaways
Concerning UI where content is dynamically displayed or filtered based on user input from a text field:
- This is a proposal of behavior. You might need modifications for your specific use case. Get user feedback.
- Provide people an instructional cue (accessible description) that "results will [adjective] as you type" for search components that dynamically render results. Use the words like "filter", "display", both or similar depending on the functionality of your component.
- Live regions only communicate strings of text. If you need to convey information that has important semantics or other accessibility information, a live region is likely the wrong way to do it.
- Constantly interrupting a user as they type with a live region should be avoided. Thus constantly announcing how many results are shown is not the best UX.
- Some people might need more time to enter a search query due cognitive or motor disabilities. Some people might quickly type what they want and then navigate away from the text field before any dynamic announcement is made. Again, another reason to not rely heavily on live regions.
- Do ensure that an assertive live region is used to immediately inform a user that no results were found. This is the one time we DO want to interrupt people, so they don't waste time typing when their query has already failed.
- A live region will have the most robust support if it is an empty live region that is populated with content, rather than the live region itself being injected when rendering a message.
- Consider other UX improvements that will help people quickly navigate your UI, which feel natural but also won't disrupt expected functionality for other controls.
It has become common place for people to be presented with web interfaces where one can type into a search or filter text field, and a script can run to (almost) immediately return relevant results on the same page.
Such an interface is useful and provides an immediacy to slower experiences of days past. Rather than require people input their query, hit submit, wait for the new page or screen of results to return and then they can determine if the results contain what they want or not… dynamically display the results, or filtering down already displayed content cuts out all the waiting.
Please note, the described UI is not referring to autocomplete text fields / comboboxes.
more info and spoilers
This post is specifically going over a search or filtering UI where people can type into a text field and results immediately display, filter, or both on the current web page. You can see an example of content dynamically filtering at a11ysupport.io.
Spoilers: a11ysupport.io employs a lot of the accessibility considerations this post outlines. Thanks @mfairchild365.
Of course, this then begs the question of how do you convey such information to people who use assistive technologies. Such as screen readers. If someone cannot see the page, how will they know when results are dynamically returned, let alone that results are going to be rendered at all?
If you are familiar with web accessibility, and in particular ARIA, you might be thinking “use a live region!”
If you are familiar with live regions, such a blanket recommendation might make you think “for the love of everything you hold dear, please do not use a live region!”
One thing at a time
Let’s back up a second and level set on the situation and come to a few base understandings.
First, live regions
Live regions are used when you need to communicate information that is being presented visually, but someone using assistive technologies (AT) might not be aware of. The situations where this would occur are for people with low or no vision, where they are specifically focused or scanning or interacting with one part of a web page, but the dynamic update is occurring somewhere else. Similarly, people with low vision who might be looking at a zoomed in, or magnified portion of their screen, but the updated visual occurs outside of their current area of visibility.
A live region can be used to communicate this information to these people if they are using AT, such as screen readers, which can ‘listen’ for dynamic changes that are exposed via live regions. However, even with live regions there are limitations to what will be communicated.
For instance, live regions are a good tool for communicating a simple string of content. E.g., “your information has been saved!” or “there was a problem with X. Please try again.”
Live regions are rubbish if you need to communicate large swaths of information, or information that would best be communicated by other semantics and accessibility properties. This is because live regions only communicate strings of text.
For example:
<div aria-live="polite">
<button>You'll have to guess what I represent!</button>
</div>
The text of the <button>
would be announced when it was injected into the live region. The fact that the text represents the label of a button would not be communicated… as there would be no mention of the button’s role in the live region announcement.
Now, the reason I mention all of that is because a common misunderstanding about how to communicate the returned results for dynamic filtering of content or search results is to wrap all of said content in a live region.
No. Gross. Never.
Consider the following:
<label>
Search help articles
<input type=text>
</label>
<h2>Results:</h2>
<div aria-live="polite">
<ul>
<li>
<a href="...">Title of article</a>
<p>maybe some description goes here</p>
</li>
<li>
<a href="...">Title of article</a>
<p>maybe some description goes here</p>
</li>
...
</ul>
</div>
If the above results were injected into a live region, someone using a screen reader would hear a flattened text string of the content as a single uninterrupted announcement. Assuming the native bullet list markers have not been removed from the results list, the announcement could resemble the following:
“bullet, Title of article maybe some description goes here. bullet, Title of article maybe some description goes here …”
So that’s awful.
And because I like making silly comparisons to explain information, it’s awful because it would be like someone asking you how many pages are in a book. Then, rather than give a concise answer, you instead shout at them the entirety of the book’s text. You do this until they literally tell you to shut up (Ctrl key), or until you run out of air and collapse.
OK, point taken. So instead we should just let people know that content has been updated, or let them know the specific number of results that are being dynamically displayed. Right?
Hmm… Maybe? Sorta? Opinions will vary, and there are still live region gotchas to consider. Read on…
Second, I’m typing. Don’t interrupt me… well, unless…
Another thing to keep in mind about this particular use case, is that someone is actively trying to enter their query into a text field. Imagine trying to type a word, but after every character you type the UI intrusively displays a message that covers the screen saying “results are available” or “X results are available”.
That might be helpful. Once. Particularly if you weren’t already aware that this is how the component behaves. But, doing that after every single character entry gets annoying real fast.
We don’t want to constantly interrupt and distract people as they type. Additionally, unless there is a number of returned results displayed in text – it’s rather pointless to convey how many results are being rendered. This is especially true when the results may be so great that the total number would be unknown, even if one could see the whole screen at once.
Now, there can be value in letting people know when they have filtered results down to just a few instances. But even if waiting until only a few results are shown, interrupting announcements are still interrupting announcements.
Who’s to say what number is the number where it becomes OK to start such behavior? Some people might want 10? Maybe five? What if all the results are very similar even at five, and the best way to make sure the right result is returned is to just finish the phrase? This is where opinions will vary and complicate things. Regardless of what you decide, someone will not be happy.
With that in mind, a solution to frequent interrupting announcements could be to only announce after a delay of inactivity. For instance, a live region could remain empty until a long enough delay since the last key press has been detected. But what is “long enough” will depend on the person. Some people might need more time to type due to cognitive or motor disabilities. Some people might quickly type what they want and then navigate away from the text field before an announcement can even be made. Sounds like opinion territory again, no?
Another thing to consider: while we don’t want to constantly interrupt people while typing, we need to interrupt when things go afoul. Specifically, let someone know immediately when they have entered a query that returns no results.
Delaying such an announcement would result in wasted time, and potential uncertainty about “when” someone’s query stopped working. Was it because of a typo? Was it because of an added word that made their query too specific? People who can see the UI are likely to notice right away when the dynamic content dries up. They can then immediately correct for this by adjusting their query. This same affordance must be provided to people with disabilities.
A proposal
People want and need different things. So, rather than try to come up with the perfect live region solution and determine the amount of time necessary to delay such dynamic announcements (which will never please everyone), consider the following proposal:
<!--
the text field to perform the search/filter.
-->
<label>
[ accessible name/label for field ]
<input id=field aria-describedby=info>
</label>
<!--
other stuff goes here. or maybe nothing.
it's your UI after all...
-->
<!--
a heading to introduce the results/content.
use the appropriate level.
-->
<h# tabindex=-1>
<!-- optionally use the query in the heading -->
Results for [query]
<!--
it can be useful to include the number of results in the heading.
style as needed with CSS.
-->
<span hidden># available</span>
</h#>
<!--
text to indicate how this UI works.
it must be associated with the text field.
-->
<div id=info>
<!--
may be visual or hidden depending on persnickety individuals.
this text could persistently go near the text field as well.
-->
Results will update as you type.
</div>
<!--
empty, unstyled live region just waiting for no results
to be displayed.
-->
<div aria-live=assertive aria-atomic=true>
<!-- if no result found, inject that message here -->
</div>
<!--
markup for rendering search results goes here.
it's whatever you need it to be.
-->
The text field
Use a text field or search field with an appropriate visible label.
Provide that field with a description to indicate that “results will update as you type” (or some similar verbage that your content team can decide upon. People are so picky with their words…).
In the markup snippet above, aria-describedby
is used to associate the description of behavior with the text field. Ideally this description is visible so it can inform everyone of what to expect. However, pick your battles if necessary. If this cannot be visible because people value their aesthetic opinions over usability, then just make sure the description is programmatically associated with the form field.
When someone navigates to the text field, the description can be conveyed to their assistive technology. This will inform this person of the behavior they can expect. Telling someone this up front sets expectations, allowing content to update without needing to constantly inform them that such content will update.
Arguably, one could make a case for appending this behavior to the end of the field’s accessible name. A valid reason being some people turn off the automatic announcement of element descriptions. I’m personally of two minds on this, as I do think it’s important to inform people of the behavior… but such information can become unnecessarily redundant the more someone is used to interacting with a particular UI.
For instance, how many of us really benefit from being informed what seat belts are and how they work every time we travel by airplane? Adding instructions to the accessible names of controls is like forcing people to listen to that. I prefer the route of providing the necessary information, but allowing the individual to easily ignore if they so choose.
The results / dynamic content section
Moving on… introducing the search results with a heading will allow people a consistent location they can expect results to populate in. This benefits everyone in the end. But people who use screen readers generally get a lay of the land (or the DOM… whatever) when they come to new web pages. Much like someone with full vision will quickly glance around a web page to get at least a basic understanding of the information presented.
I don’t care to be prescriptive in the manner in which you markup your search results. Whether they be a list, or a series of headings containing links with a descriptive sentence for more context… both can be valid, so have at it.
Communicating no results
While we do not want to use live regions to constantly interrupt people as they type, there is benefit to interrupting someone when no results are found. If a message is displayed to indicate to indicate a no results state, then it actually becomes a WCAG 2.1 requirement that the status message is announced.
The live region is an empty <div aria-live=assertive>
. I suggest having a generic live region rather than using a specific live region role
.
Context for suggesting a generic live region
I personally don't see much of a reason to use a role for such a message, but I specifically don't want to use a role=alert
.
While an alert
would create an assertive live region, some screen readers and browsers will immediately announce the existence of an "alert" on page render (I'm specifically looking at you, TalkBack).
Essentially, an "alert" announcement will be made immediately (the element's role). But as it has no content yet... because nothing's gone wrong at this point, there's no indication as to why this "alert" was announced.
Granted, such behavior should be considered a bug... but I don't want to deal with that nonsense. There's too many other things to worry about, so I suggest we just avoid in this instance. Do file a bug if you like, though :)
Due to live regions being notoriously awful at being announced when injected into a web page, ensure an empty live region that content can be injected into exists in the DOM. Using live regions in this manner offers the most robust support to ensure their announcement will be made by as many screen reader and browser combinations as possible.
Wait. What happens if I press Enter?
Ah, good question. That’ll depend on the type of search component you are creating. Essentially, there are two types of dynamic search or filtering components.
First, there may be instances of components like these that return “quick results”. Someone entering their query might get common suggested results, but if they hit enter, or activate a “submit button”, the query will be sent and a full results page will be rendered. So in that case, Enter essentially works as generally expected. It submits the search query.
For the second type,results are only shown and filtered on the current page. There is no formal submission of a query. But muscle memory is weird… and unless you explicitly mention that there is no form submission, people might try it out anyway.
Most will then realize, when a whole bunch of nothing happens, that what they get is what they get. They’ll then move on and start interacting with the content of the page.
But, we could go beyond the bare minimum of making a WCAG-conforming user experience and provide some extra functionality for people who might want or expect Enter to do something?
Just because WCAG says you only have to give someone a turkey sandwich, that doesn’t mean you can’t give them condiments to go with it.
By listening for an Enter key press, JavaScript can programmatically move focus to the heading that introduces the result listing. If placing the number of found results within the heading, then that information will be announced along with the static heading text.
const field = document.getElementById('field');
field.addEventListener('keyup', function ( e ) {
if ( e.key == 'Enter' ) {
heading.focus();
}
});
While we’re at it, why not provide a quick mechanism to return to the search field as well? In discussing this pattern with one of my coworkers, he found himself wanting to press the Esc key to return to the search field. Even though he was aware that was unexpected functionality for such a UI, it just seemed to fit how he’d expect this to work.
Now, one person’s expectation doesn’t make for a standard, but it was a good idea. More importantly, implementing this functionality generally won’t disrupt anyone’s experience, and it’ll be useful for those who had similar expectations.
heading.addEventListener('keyup', function ( e ) {
if ( e.key == "Escape" ) {
field.focus();
}
});
The one caveat to this is to make sure that if you have this UI within a dialog, or some other form of “popup” that will close if pressing the Esc key, that you make sure to suppress this functionality when the heading is focused.
The outcome
And with that, the following represents a quick demo of expected behavior. The same results will return no matter what you enter into the text field, and a no results found message will appear as soon as more than 5 characters are detected in the text field.
See the Pen quick demo of showing / informing about dynamic results by Scott (@scottohara) on CodePen.
You can also check out a11ysupport.io which has implemented ideas from this post into the search/filter field there. You know, if you want to check out a real example, rather than my sloppy demo trash :)