A11y
Announce the new page to screen readers, move focus to the content and respect reduced motion.
Import it from pagefold/a11y (about 1.9 KB minified and compressed, shared code included).
import PagefoldA11y from 'pagefold/a11y'
new Pagefold({ plugins: [new PagefoldA11y()] })
What it does
- Announces the page: a visually hidden live region (
role="status",aria-live="assertive", orpolitewith thepolitenessoption) is filled with your message after each visit. - Moves focus to the top of the new page (the
body, or thecontentSelectoryou choose), so keyboard and screen reader users start at the top of the new page and not on a link that no longer exists. Focus moves even when the link that was clicked has it. - Respects reduced motion. If the user asked for less motion, visits are not animated: Pagefold fires
animation:skipand swaps the content straight away. - Focuses anchors. A link to
#sectionon the same page moves focus to that section.
Where focus goes
After the content is replaced, Pagefold asks this for the focused element:
- Is focus in a text field, a select or a contenteditable element?Yes → stays
- Is it inside something marked data-pagefold-persist?Yes → stays
- Focus moves to the body, or to contentSelector.
Yes: Focus is left where it is. People should not be interrupted while typing.
When it leaves focus alone
If focus is in a text field, a select, a contenteditable element or inside something marked data-pagefold-persist, it stays there. People should not be interrupted while typing.
The temporary tabindex="-1" it adds to the content is removed when focus leaves.
What is announced
The plugin looks for the name of the page in this order and says the first it finds:
- The
aria-labelof the heading:<h1 aria-label="Homepage">Project title</h1>is read as “Homepage”. It is the easiest way to say something different from the heading. - The text of the heading.
- The document title.
- If there is none, the address, with the
urltext.
headingSelector can be a list. The default looks inside main first, so a logo in an h1 outside it does not win.
Several languages
Key the texts by the language of the page. It must match the lang of the html element exactly, and * is the fallback:
new PagefoldA11y({
announcements: {
'en-US': { visit: 'Navigated to: {title}', url: 'New page at {url}' },
'de-DE': { visit: 'Navigiert zu: {title}', url: 'Neue Seite unter {url}' },
'*': { visit: '{title}', url: '{url}' },
},
})
Pagefold does not change the lang for you. The Head plugin does it, or you can in content:replace.
Changing it for one visit
The plugin gives every visit an a11y object. Use it to change what is announced or where focus goes:
// The text is only known once the new page is there, so change it just before it is said
pagefold.hooks.before('content:announce', (visit) => {
visit.a11y.announce = 'Results updated' // or false to say nothing
})
pagefold.hooks.on('visit:start', (visit) => {
// Focus <main>, and wait until the visit is over instead of doing it right after the swap
visit.a11y.focus = { selector: 'main', wait: true }
// visit.a11y.focus = false turns focus handling off for this visit
})
The hooks content:announce and content:focus fire when the announcement is made and when focus moves. A fragment rule can name the element to focus with focus: '#detail', or focus: false.
Announcing something yourself
When the page changes without a visit, such as a filter on a list, say so:
pagefold.announce?.('Filtered by cats')
Tip
Use a heading in each page’s content. The announcement reads the first match of headingSelector, so a clear h1 makes the message useful.
Common problems
- The announcement is empty or says the wrong thing. The title comes from the first match of
headingSelector. Make sure each page has oneh1. - An outline appears around the content after a visit. Focus moves there so keyboard users start at the top. The content is not interactive, so you can hide the ring:
#pagefold:focus { outline: none }. - Animations do not play for some visitors. They asked their system for reduced motion, and the plugin respects it.
Options
| Option | Type | Default | What it does |
|---|---|---|---|
announce | boolean | true | Tell screen readers about the new page. |
announcements | { visit, url } | Record<lang, { visit, url }> | { visit: 'Navigated to: {title}', url: 'New page at {url}' } | What is said. visit is used when the page has a title, url when it has none. Key it by language for several. |
headingSelector | string | string[] | ['main h1', 'h1'] | Where to look for the heading, in order. The document title is the fallback. |
autofocus | boolean | false | Focus the element with an autofocus attribute in the new content. |
focus | boolean | true | Move focus to the new content after a visit. |
contentSelector | string | 'body' | The element that receives focus. Use 'main' or a container selector to focus the new content instead. |
politeness | 'assertive' | 'polite' | 'assertive' | How urgently screen readers read the announcement: assertive interrupts, polite waits. |
respectReducedMotion | boolean | true | Skip animations when the user prefers reduced motion. |
inertLeaving | boolean | true | Make the content that is leaving inert while it animates out, so it cannot be clicked or focused. It is given back if the visit is aborted. Elements the page made inert itself are left alone. |