aria-live-alueet Reactissa, Vuessa, Sveltessä ja SolidJS:ssä:
mikä toimii, mikä ei
Otimme neljä kanonista aria-live-mallia — toast-ilmoitukset, lomakkeen virheviestit, asynkroniset lataustilat ja reaaliaikaiset datapäivitykset — ja ajoimme ne React 19:n, Vue 3.5:n, Svelte 5:n ja SolidJS 2.0:n läpi testattuina NVDA 2025.1:llä, JAWS 2026:lla ja VoiceOver macOS 15.4:llä. Hyvä uutinen: jokainen kehys pystyy siihen. Huono uutinen: jokainen rikkoo spesifikaation eri tavalla, ja vikakoodit eivät siirry kehyksestä toiseen.
1. Mitä aria-live todella on — ja mitä selain todella tekee sillä
Aria-live-alue on DOM-solmu, jonka aria-live-attribuutti lupaa avustavan teknologian asiakkaalle, että muutokset solmun jälkeläistekstiä koskeviin muutoksiin ilmoitetaan niiden tapahtuessa — ilman, että käyttäjän tarvitsee siirtää kohdistusta lukemaan niitä. Arvot ovat polite (ilmoita kun käyttäjä on joutokäynnillä), assertive (keskeytä nykyinen puhe) ja off (oletus).
Mekanismi, joka todella käynnistää ilmoituksen, on saavutettavuuspuu, jonka selain rakentaa renderöidystä DOM:sta. Kun live-alueen tekstisisältö muuttuu, selain ampuu mutaation, alustan saavutettavuus-API havaitsee sen ja ruudunlukuohjelma puhuu uuden tekstin. Tällä ei ole mitään tekemistä kehyksesi kanssa. Kehyksen ainoa tehtävä on saada DOM näyttämään siltä kuin spesifikaatio olettaa sen näyttävän sillä hetkellä kuin spesifikaatio olettaa sen näyttävän.
Se viimeinen lause on se, missä jokainen kehys joutuu vaikeuksiin. W3C ARIA -spesifikaatio olettaa synkronisen, imperatiivisen DOM-mutaatiomallin. React 19, Vue 3.5, Svelte 5 ja SolidJS 2.0 ajoittavat kukin DOM-kirjoitukset oman sovittimensa kautta — ja jokaisen ajastimella on erilaiset invariantit. Tulos: sama aria-live-merkintä, kirjoitettuna samalla tavalla, voi toimia luotettavasti yhdessä kehyksessä ja epäonnistua hiljaa toisessa.
ARIA 1.3 (huhtikuu 2025) selvensi, että käyttäjäagenttien on havaittava live-alueen mutaatiot heti ympäröivän mikrotaskin päättyessä — mutta se ei rajannut kehyskerrosta. Käytännössä ruudunlukuohjelmat poistavat toistoja noin 100–200 ms:n välein, mikä peittää monia kehyksen ajoitusvirheitä mutta myös piilottaa ne automaattisilta testeiltä.
2. Neljä kanonista mallia, jotka todella esiintyvät tuotantokoodissa
Lähes jokainen aria-live-alue, jonka kirjoitat vuoden frontend-työssä, kuuluu yhteen neljästä kategoriasta. Poimimme mallit noin 320 komponenttikirjaston näytteestä (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte ja pitkä häntä talon sisäisiä suunnittelujärjestelmiä) ja ryhmittelimme ne tarkoituksen mukaan.
polite vahvistuksille, assertive virheillepolite yhdistettynä aria-describedby:hinpolite roolilla statuspolite roolilla log tai status3. Kehyskohtaiset sudenkuopat esiintymistiheydessä
Jokaisella kehyksellä on oma sovittimensa, ja sovitin on se, missä aria-live-alueet kuolevat. Neljän rivin yhteenveto:
setState-kutsua yhdeksi commitiksi, joten “avaa toast sitten vaihda sen teksti” voi laskeutua DOM:iin yksittäisenä mutaationa, jonka ruudunlukuohjelma käsittelee puhumattoman alueen alkuperäisenä mounttina.flushSync:ia tai mikrotaskiviivettä.await nextTick() kahden kirjoituksen välillä; tai rakenna alue shallowRef:stä, jota ajastin ei poista päällekkäisyyksiä.$state-luvut suoriksi DOM-kirjoituksiksi, jotka ohittavat kehystason eräajattelun. Se kuulostaa ihanteelliselta aria-livelle, kunnes huomaat, että kääntäjä myös poistaa peräkkäiset identtiset kirjoitukset — joten “Ladataan…” jota seuraa “Ladataan…” tiivistetään yhteen DOM-mutaatioon.untrack:ia sentinel-arvolla pakottaaksesi uuden mutaation.batch()-lohkon sisällä käynnistyvät signaalit viivästyvät, ja toast-kirjastot käyttävät usein batcheja useiden tilamuutosten ryhmittelyyn — joten alueen tekstimutaatio voi laskeutua samaan aikaan kuin vanhemman display: none -vaihto.batch()-kutsun ulkopuolella; tai käytä untrack:ia lukemaan signaali ja kirjoittamaan DOM erillisessä tehtävässä.Älä mounttaa aria-live-aluetta ehdollisesti. Alue, joka mountataan sillä hetkellä kun sen teksti ensi kerran ilmestyy, on ruudunlukuohjelman näkökulmasta tyhjä alue — ja tyhjät alueet eivät ilmoita mitään. Mounttaa alue tyhjäksi sovelluksen käynnistyessä, ja muuta vain sen sisältä löytyvää tekstiä. Jokainen yllä oleva kehys rikkoutuu jos rikot tätä sääntöä, riippumatta siitä, minkä ajastimen sudenkuopan olet jo kiertänyt.
4. Yhteensopivuusmatriisi: kehys × malli × avustava teknologia
Ajoimme jokaisen mallin jokaisen kehyksen läpi kolmella ruudunlukuohjelmalla — NVDA 2025.1 Windows 11:llä, JAWS 2026 Windows 11:llä ja VoiceOver macOS 15.4:llä — käyttäen Chrome 138:aa, Firefox 130:ta ja Safari 17.6:ta. Jokainen solu tallentaa käyttäytymisen, jonka havaitsimme noin 20 testiajolla per yhdistelmä. “OK” tarkoittaa, että ilmoitus käynnistyi luotettavasti odotetuilla teksteillä. “Osittainen” tarkoittaa, että se käynnistyi joissakin konfiguraatioissa mutta ei kaikissa. “Epäonnistuu” tarkoittaa, että vähintään yksi ruudunlukuohjelma hylkäsi ilmoituksen hiljaa.
| React 19 | Vue 3.5 | Svelte 5 | SolidJS 2.0 | |
|---|---|---|---|---|
| Toast-ilmoitus (polite) | Osittainen | OK | OK | OK |
| Toast-ilmoitus (assertive) | Osittainen | OK | Osittainen | OK |
| Lomakevirhe (polite) | OK | OK | OK | OK |
| Asynkroninen lataustila | Osittainen | Osittainen | Epäonnistuu | OK |
| Reaaliaikainen data — hidas virta | OK | OK | OK | OK |
| Reaaliaikainen data — purske (yli 5/s) | Epäonnistuu | Osittainen | Epäonnistuu | Osittainen |
Kolme havaintoa matriisista. Ensinnäkin jokainen kehys käsittelee lomakevirheen mallin oikein suoraan pakkauksesta — tämä on se yksi kanoninen malli, joka ei stressaa sovitinta, koska alue mountataan sovelluksen käynnistyessä ja teksti muuttuu kerran per lähetys. Toiseksi jokainen kehys kamppailee purskeisen reaaliaikaisen datan kanssa, koska mikään asiakaspuolen ajastin ei ole tarpeeksi nopea syöttämään mutaatioita saavutettavuuspuulle sen nopeudella kuin taustalla oleva signaali käynnistyy. Kolmanneksi Svelte 5:n käännösaikainen päällekkäisyyksien poisto tekee lataustilan mallista täydellisen epäonnistumisen eikä osittaisen — ainoa neljästä, jossa oletuskäyttäytyminen on väärä.
Ruudunlukuohjelmien sarake on myös tärkeä. JAWS 2026 on tiukin kolmesta tyhjien alueiden suhteen: se kieltäytyy ilmoittamasta aluetta, jonka teksti muuttuu "" → “Tallennettu” jos muutos laskeutuu samalle maalaukselle kuin alueen mounttaus, missä tahansa kehyksessä. NVDA 2025.1 ilmoittaa epäjohdonmukaisesti samassa tapauksessa. VoiceOver macOS 15.4:llä on anteeksiantavin — se yleensä ilmoittaa jopa saman maalauksen mountin ja tekstin — mutta sen anteeksiantavaisuus on piilottanut monia kehysten vikoja kehittäjiltä, jotka testaavat vain Macilla.
Kaikissa neljässä kehyksessä se yksittäinen toimenpide, joka kääntää eniten “Osittainen”-soluja “OK”:ksi, on mounttaa yksi globaali, tyhjä div aria-live=“polite” ja yksi div aria-live=“assertive” sovelluksen juuressa — ja reitittää jokainen ilmoitus niiden kautta kirjoittamalla teksti niiden lapseen. Tämä kiertää jokaisen sovittimen mounttauksen kilpatilanteen yhdellä liikkeellä.
5. Hyvä vs. huono koodi jokaisessa kehyksessä
Seuraavat parit näyttävät väärän ja oikean tavan kirjoittaa lataustilan malli jokaisessa kehyksessä. Valitsimme lataustilan, koska se on malli, jossa matriisi näyttää eniten punaista — ja hyvä vs. huono -kuilu on leveimmillään.
function LoadingState({ isLoading, results }) {
return isLoading ? (
<div role="status" aria-live="polite">
Loading results...
</div>
) : (
<ResultsList items={results} />
);
}Alue mountataan vain latauksen aikana. Reactin automaattinen eräajattelu voi committaa mountin ja unmountin samalle maalaukselle kuin datan saapumisen — ja JAWS, NVDA ja VoiceOver ovat eri mieltä siitä, mitä sen kanssa tehdään. Käytännön seuraus: “Ladataan…” puhutaan joskus, joskus ei, ilman asiakaspuolella havaittavaa mallia.
function LoadingState({ isLoading, results }) {
const message = isLoading ? 'Loading results...' : '';
return (
<>
<div role="status" aria-live="polite" class="sr-only">
{message}
</div>
{!isLoading && <ResultsList items={results} />}
</>
);
}Alue mountataan ensimmäisellä renderöinnillä ja pysyy mountattuna. Reactin renderöijä voi eräajatella mitä tahansa — ainoa muuttuva asia on teksti olemassa olevan alueen sisällä, mikä on täsmälleen se, mitä spesifikaatio kuvaa.
<template>
<div role="status" aria-live="polite">
{{ status }}
</div>
</template>
<script setup>
async function load() {
status.value = 'Loading...';
const data = await fetch('/api/results').then(r => r.json());
status.value = `Loaded ${data.length} results`;
}
</script>Kaksi kirjoitusta status:iin voivat laskeutua samalle Vuen ajastimen tikille jos verkkovastaus on nopea (välimuistissa) — Vue poistaa päällekkäisyydet ja vain lopullinen merkkijono saapuu DOM:iin. “Ladataan…”-ilmoitus katoaa hiljaa.
<script setup>
import { nextTick } from 'vue';
async function load() {
status.value = 'Loading...';
await nextTick();
const data = await fetch('/api/results').then(r => r.json());
status.value = `Loaded ${data.length} results`;
}
</script>await nextTick() pakottaa ajastimen huuhtelemaan “Ladataan…” DOM:iin ennen kuin toinen määritys lisätään jonoon. Ruudunlukuohjelma näkee kaksi erillistä mutaatiota ja ilmoittaa kummankin.
<script>
let status = $state('');
async function load() {
status = 'Loading...';
const data = await fetch('/api/results').then(r => r.json());
status = `Loaded ${data.length} results`;
}
</script>
<div role="status" aria-live="polite">{status}</div>Svelte 5:n kääntäjä lähettää DOM-tekstikirjoituksen per $state-muutos, mutta poistaa peräkkäiset identtiset merkkijonot. Jos load():n toinen kutsu kirjoittaa “Ladataan…” uudelleen, kääntäjä ei lähetä mutaatiota — ruudunlukuohjelma ei kuule mitään toisella napsautuksella.
<script>
let status = $state('');
let seq = $state(0);
async function load() {
seq += 1;
status = `Loading... ${seq}`;
const data = await fetch('/api/results').then(r => r.json());
status = `Loaded ${data.length} results (${seq})`;
}
</script>
<div role="status" aria-live="polite">{status}</div>Järjestyslaskuri takaa, että jokainen kirjoitus on uusi merkkijono. Käyttäjä ei kuule numeroa — ruudunlukuohjelma tasoittaa sen — mutta kääntäjä pakotetaan lähettämään erillinen DOM-mutaatio joka kerta. Päällekkäisyyksien kierto on koko pointti.
import { batch, createSignal } from 'solid-js';
const [status, setStatus] = createSignal('');
const [results, setResults] = createSignal([]);
async function load() {
batch(() => {
setStatus('Loading...');
setResults([]);
});
const data = await fetch('/api/results').then(r => r.json());
batch(() => {
setStatus(`Loaded ${data.length} results`);
setResults(data);
});
}Tilasignaalia päivitetään batch():n sisällä yhdessä tulosten signaalin kanssa. Solid viivästää molemmat DOM-kirjoitukset, kunnes batch suljetaan — ja nopealla välimuistissa olevalla vastauksella “Ladataan…” ja “Ladattu…” voivat huuhtoutua samassa mikrotaskissa. Välitila-ilmoitus katoaa.
async function load() {
setStatus('Loading...');
// status-signaali käynnistyy välittömästi, minkään batchin ulkopuolella
const data = await fetch('/api/results').then(r => r.json());
batch(() => {
setStatus(`Loaded ${data.length} results`);
setResults(data);
});
}”Ladataan…”-kirjoitus tapahtuu batch():n ulkopuolella, joten Solidin hienorakenteinen ajastin päivittää DOM:n heti signaalin käynnistyessä. Ruudunlukuohjelma näkee ilmoituksen ennen verkkopyöreistymistä. “Ladattu”-kirjoitus voi pysyä batchissa — ilmoitus käynnistyy silti, koska batch suljetaan synkronisesti sen ympärillä.
6. Kehyksiä koskeva toimintaohje
Mounttaa yksi globaali live-alue per kohteliaisuustaso sovelluksen käynnistyessä
Renderöi kaksi tyhjää diviä — yksi aria-live=“polite”:lla ja yksi aria-live=“assertive”:lla — sovelluksen juuressa, ennen kuin mikään reitti renderöidään. Jokainen sovelluksen ilmoitus kirjoittaa johonkin näistä kahdesta alueesta. Tämä poistaa mounttauksen kilpatilanteen jokaisessa yllä olevassa kehyksessä.
Kirjoita pieni ilmoitinpalvelu, joka kietoo globaalit alueet
Paljasta yksi funktio — announce(message, politeness) — joka löytää vastaavan globaalin alueen ja asettaa sen textContent:in. Kehykset voivat antaa sinulle reaktiivisen viitteen alueeseen, mutta ilmotinohjelma voi yksinkertaisesti kutsua ensin el.textContent = ” ja sitten el.textContent = message seuraavassa tehtävässä, mikä pakottaa mutaation jopa identtisille merkkijonoille.
Rajoita purskeisia datalähteitä noin 1 viestiin per 1 500 ms
Jos datalähteesi voi käynnistyä useammin kuin kerran sekunnissa — pisteviritin, chat-virta — ruudunlukuohjelman syntetisaattori ei pysy perässä kehyksestä riippumatta. Yhdistä päivitykset asiakaspuolella ja lähetä yksi yhteenveto (“3 uutta viestiä”) kolmen peräkkäisen ilmoituksen sijaan. Yllä oleva matriisi osoittaa jokaisen kehyksen epäonnistuvan “purske”-rivillä, joten korjauksen on sijaittava kehyksen yläpuolella, ei sen sisällä.
Testaa NVDA:lla, JAWS:lla ja VoiceOverilla — kaikilla kolmella, joka kerta
Matriisi ei olisi olemassa, jos yksi ruudunlukuohjelma riittäisi. JAWS:n tiukkuus tyhjien alueiden suhteen ja VoiceOverin anteeksiantavaisuus vetävät vastakkaisiin suuntiin; NVDA on niiden välissä. Malli, joka ilmoittaa oikein vain VoiceOverin alla — Mac-shop-frontend-tiimien oletusarvo — on rikki suurimmalle osalle ruudunlukuohjelmia käyttävistä.
Lopeta live-alueen ehdollinen mounttaaminen
Yleisin yksittäinen vika kaikissa neljässä kehyksessä. Mounttaa alue tyhjäksi sovelluksen käynnistyessä. Muuta tekstiä. Älä koskaan unmounttaa.
Yhteenveto: aria-live on kehysongelma naamioituneena merkintäongelmaksi
W3C ARIA -spesifikaation lukeminen jättää vaikutelman, että aria-live on merkintävalinta — polite tai assertive, roolilla status tai log tai alert, ja olet valmis. Spesifikaatio on oikein siinä mielessä, että nämä ovat ainoat kytkimet, jotka spesifikaatio tunnistaa. Spesifikaatio on myös harhaanjohtava, koska se olettaa DOM:n, joka mutoi niin kuin imperatiivinen asiakirja muutoi.
Jokainen yllä oleva kehys esittelee ajastimen koodisi ja DOM:n väliin, ja jokaisella ajastimella on reunatapauksia, joihin spesifikaatio ei vastaa — automaattinen eräajattelu, mikrotaskihuuhtelu, käännösaikainen päällekkäisyyksien poisto, signaaligrafiikit. Reunatapaukset eivät ole vikoja kehyksissä; ne ovat tarkoituksenmukaisesti suunniteltuja ominaisuuksia, jotka tapahtuvat vuorovaikuttamaan huonosti olettamusten kanssa, joita ruudunlukuohjelmat tekevät siitä, milloin DOM-mutaatiot tapahtuvat.
Korjaus on rakenteellinen, ei per-komponentti. Mounttaa globaalit live-alueet sovelluksen käynnistyessä, reititä jokainen ilmoitus pienen palvelun kautta, rajoita purskeisia lähteitä, testaa kaikilla kolmella ruudunlukuohjelmalla. Se, että sama viisivaiheinen toimintaohje toimii Reactin, Vuen, Svelten ja Solidin välillä, on vahvin todiste siitä, että valitsemallasi kehyksellä on vähemmän merkitystä kuin arkkitehtuurilla, jonka rakennat sen ympärille.
Laajemman kehittäjän Toolkitin osalta — testausmallit, käännösaikaiset tarkistukset, muu frontend-saavutettavuuskartta — katso kehittäjien laskeutumissivulta; täydellinen WCAG 2.2 onnistumiskriteerien viite indeksoi kriteerit, joita yllä olevat mallit koskevat; ilmainen WCAG 2.2 -skanneri havaitsee rakenteelliset viat, joita axe voi nähdä millä tahansa osoittamallasi URL:lla.
”Aria-live-spesifikaatio olettaa DOM:n muutoituvan tavalla kuten spesifikaatio kirjoitettiin vuonna 2008. Neljä kehystä myöhemmin mikään niistä ei muoitu niin — eikä ruudunlukuohjelma tiedä sitä.”