It really depends whether you are just adding some messages or replacing large parts of the page.
Messages
There are Aria Live Regions, which announce any change to their contents. This is very useful for status messages and sometimes even used with visually hidden live regions to only address screen reader users.
<button onclick="document.querySelector('#statusbar').innerHTML = new Date().toString()">Update</button>
<div id="statusbar" aria-live="assertive"></div>
The aria-live attribute establishes a live region and its value is a politeness setting, which regulates how likely it is the change will interrupt what is currently being spoken by the screen reader.
Another classic example is inline validation of form fields, where the alert role, a live region, is used to immediately announce the error message to the user:
<label>Day of the week we hate
<input type="text" aria-describedby="error">
</label>
<div role="alert" id="error" hidden>only Monday is permitted</div>
Large Parts of Content
When JavaScript is changing large parts of the site, like in single page applications, putting everything inside a live region would be overkill and actually very annoying.
To let the user know that content changed after activating a trigger, two approaches exist:
- A new state of the trigger is announced, implying that the user can simply continue reading to find the new content
- Focus is put either onto the element who’s content changed, or on the first interactive element inside
Simply read on
The first case would be applied if the role of the trigger (or other status information) makes it clear that a content change will happen, so it’s expected.
A classic example is the accordion. It has aria-expanded state, which communicates whether its contents are currently visible or not. If they are, the user will simply continue reading, because contents should follow immediately after.
toggleAccordion = e => {
const target = document.getElementById(e.currentTarget.getAttribute('aria-controls'));
e.currentTarget.setAttribute('aria-expanded', ! target.toggleAttribute('hidden'));
}
<!-- soon to be replaced by <details> and <summary> -->
<button aria-expanded="false" aria-controls="accordion-content" onclick="toggleAccordion(event)">2.1 First Rule of ARIA Use</button>
<blockquote id="accordion-content" hidden>
<p>If you can use a native HTML element [HTML51] or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so. […]</p>
</blockquote>
Put focus on the new content
In the second case focus is set programmatically elsewhere, so that element will be announced. This is particularly helpful if it’s parent elements have grouping roles, so their names will be announced as well, as in the case of a modal dialog.
Another example would be a single page application’s navigation, where the single navigation items are still navigated by means of tab.
To be able to programmatically focus a non-interactive element, but not manually, tabindex="-1" is necessary. Focussing the headline is a best practice.
/* some sort of SPA router */
document.querySelectorAll('nav a').forEach(a => a.addEventListener('click', e => {
// hide all visible contents
document.querySelectorAll('main > :not([hidden])').forEach(c => c.hidden = true);
document.querySelectorAll('[aria-current]').forEach(c => c.removeAttribute('aria-current'));
// show selected content
const content = document.querySelector(e.currentTarget.getAttribute('href'));
content.hidden = false;
content.querySelector('h1').focus();
e.currentTarget.setAttribute('aria-current', 'page');
}));
a[aria-current] { font-weight: bold }
<nav>
<ul>
<li><a href="#page-1" aria-current="page">Page 1</a></li>
<li><a href="#page-2">Page 2</a></li>
</ul>
</nav>
<main>
<div id="page-1">
<h1 tabindex="-1">Page 1</h1>
<p>Many lines of content to follow</p>
</div>
<div id="page-2" hidden>
<h1 tabindex="-1">Page 2</h1>
<p>Many lines of content to follow</p>
</div>
</main>