Utilisation autonome
Quand l’utiliser
Pour une vérification manuelle d’une page ouverte dans un vrai navigateur, un bookmarklet, ou tout autre contexte sans pilote d’automatisation ni étape de build à disposition.
Utilisation
@surea11y/core fournit surea11y.browser.js à la racine du paquet, un bundle généré à partir des mêmes sources de règles que le paquet Node. Le charger directement définit une seule variable globale, a11ycore :
<script src="node_modules/@surea11y/core/surea11y.browser.js"></script><script>const result = a11ycore.runa11yCoreInPage(location.href, null, {}, null);console.log(result.checksResults.filter((r) => r.outcome === 'fail'));</script>
a11ycore.runa11yCoreInPage est exactement la même fonction que celle qu’appellent les bindings en interne : le bundle sert uniquement à la charger sans require ni système de modules, pas à ajouter une API distincte. Les arguments sont les mêmes que partout ailleurs. Une analyse limitée à une région et à WCAG 2.2 A et AA, qui laisse de côté deux widgets bruyants :
<script src="node_modules/@surea11y/core/surea11y.browser.js"></script><script>const result = a11ycore.runa11yCoreInPage(location.href,'#main', // contextSelector: scan only this region{excludeSelectors: ['#cookie-banner', '.intercom-launcher'],contrast: { mode: 'auditorAssist' } // trade some false-positive protection for more findings},{ wcag: { version: '2.2', level: 'AA' } } // runOnly);</script>
Le bundle ne contient que l’anglais. Chaque autre langue est fournie à côté, dans son propre fichier : chargez ce fichier après le bundle et la langue devient disponible :
<script src="node_modules/@surea11y/core/surea11y.browser.js"></script><script src="node_modules/@surea11y/core/surea11y.i18n.de.js"></script><script>const result = a11ycore.runa11yCoreInPage(location.href, null, { locale: 'de' }, null);</script>
Si vous demandez une langue dont vous n’avez pas chargé le fichier, vous obtenez l’anglais, pas une erreur. Voir Langues.
Le bundle se charge aussi comme un module : import a11ycore from '@surea11y/core/browser', un import nommé de runa11yCoreInPage et require() donnent tous accès à son API, et la variable globale reste définie.
Délibérément exclus du bundle : runa11yCoreAcrossFrames/a11yCoreEnableFrameResponder (l’analyse entre frames exige que la frame intégrée charge elle aussi le moteur et donne son accord, ce qui ne convient pas à une simple balise script ajoutée à la page). Utilisez directement le paquet npm si vous en avez besoin : voir INTEGRATION.md.
Appeler directement require('@surea11y/core'), sans aucun binding, est l’autre voie autonome : ce sont les Pattern 1 et Pattern 2 de INTEGRATION.md, à savoir jsdom dans Node sans vrai navigateur, ou piloter vous-même Puppeteer/Playwright au lieu de passer par un binding. Pour les tests, runa11yCoreOnHtml(html, options) de @surea11y/core/testing analyse une chaîne HTML dans jsdom et prépare le document pour vous ; depuis core 1.12, son option contentType lit un document SVG ou XHTML comme ce qu’il est.
Depuis la console du navigateur
Pour une analyse à la main, le bundle offre quelques outils. Chargez-le dans n’importe quelle page ouverte depuis la console du navigateur (l’onglet Console des outils de développement), puis analysez, listez les constats, encadrez-les sur la page ou ouvrez le rapport HTML :
// Pasted into the console of the page to check.const s = document.createElement('script');s.src = 'https://cdn.jsdelivr.net/npm/@surea11y/core/surea11y.browser.js';document.head.append(s);// Once it has loaded:a11ycore.version; // the engine's versionconst result = await a11ycore.run(); // scan the pagea11ycore.table(result); // the findings, worst firsta11ycore.highlight(result); // box each one on the pageawait a11ycore.openReport(result); // the HTML report, in a new tabawait a11ycore.inspect('headings'); // what a person checks by hand
run(options) accepte les options du moteur et deux qui lui sont propres, context (un sélecteur, pour n’analyser qu’une partie de la page) et runOnly, et récupère avant l’analyse les feuilles de style que la page ne peut pas lire elle-même. table() affiche une ligne par constat : d’abord les échecs, puis les questions qui penchent vers l’échec, celles qui penchent vers la réussite, et celles que seule une personne peut juger. highlight() encadre chaque élément que nomme un fail (rouge plein) ou un cantTell (ambre en pointillés), dans une couche à part qui ne touche pas aux éléments de la page ; clearHighlights() la retire. inspect() dessine ce qu’une personne vérifie à la main, et ne juge rien : 'headings', 'landmarks', 'names' ou 'focus-order'. openReport() récupère surea11y.report.js à côté du bundle la première fois, et le rapport montre comment vérifier chaque question. La Content Security Policy d’une page peut bloquer un script venant d’un CDN : chargez alors les fichiers depuis l’origine de la page, ou analysez avec un driver. Toutes les options sont dans la section sur la console du navigateur d’INTEGRATION.md, le guide d’intégration sur GitHub.
Packs dans une page
Un pack est préparé dans Node : il parvient donc à une page sous forme de script. Pour une page qui charge ses scripts sous forme de fichiers (une balise <script>, un bookmarklet, une extension), construisez un seul fichier dans Node : buildBrowserBundle({ packs }), de @surea11y/core/pack, est le bundle suivi des packs enregistrés. Nommez ensuite les packs dans engineOptions.packs sous la forme name@version. Le code des règles est écrit dans le script sous forme de fonctions, il s’exécute donc là où une Content Security Policy interdit eval.
// In Node, once per release of core or of a pack.const fs = require('node:fs');const { buildBrowserBundle } = require('@surea11y/core/pack');const acmePolicy = require('./acme-policy-pack');fs.writeFileSync('surea11y-acme.js', buildBrowserBundle({ packs: [acmePolicy] }));
<script src="surea11y-acme.js"></script><script>const result = a11ycore.runa11yCoreInPage(location.href, null, {packs: ['@acme/a11y-policy@1.0.0'],profile: 'acme-web-2026'}, null);</script>
Le script d’un pack ne s’exécute qu’avec la version de core qui l’a écrit : il contient le catalogue que forment core et les packs, vérifié dans Node. Une page dont le bundle est d’une autre version de core lance l’analyse sans les packs et les liste chacun dans skippedPacks, en nommant les deux versions. Un seul fichier contient les deux : ils ne peuvent donc pas se désynchroniser. Reconstruisez-le quand vous mettez à jour core ou un pack, et mettez à jour le name@version que nomme la page.
Si le bundle de core est déjà dans la page, écrivez packScript(packs) dans un fichier avec la même version de core, et chargez-le après le bundle (et dans chaque frame que vous analysez) :
// In Node, with the core whose bundle the page loads.const fs = require('node:fs');const { packScript } = require('@surea11y/core/pack');const acmePolicy = require('./acme-policy-pack');fs.writeFileSync('acme-pack.js', packScript([acmePolicy]));
<script src="node_modules/@surea11y/core/surea11y.browser.js"></script><script src="acme-pack.js"></script>
La page exécute exactement l’ensemble de packs qu’un appel à packScript a enregistré, avec la version de core avec laquelle il a été préparé : passez à un seul appel tous les packs dont une analyse a besoin. Les packs nommés que la page n’exécute pas sont listés dans skippedPacks avec la raison. Les bindings font tout cela pour vous avec .withPacks().
Attendre la page avant une analyse
Une analyse lit la page telle qu’elle est à cet instant. Sur une page qui vient de s’ouvrir, les polices web peuvent encore se charger et les images ne pas être arrivées : la même page analysée une seconde plus tard peut donner un autre résultat. L’analyse le consigne dans engine.environment.fonts et .images (voir le schéma de sortie), mais le mieux est de ne pas analyser avant que la page se soit stabilisée.
waitForPageReady(options) se charge de cette attente, et le bundle la place sur le même global. L’analyse ne l’appelle jamais elle-même ; appelez-la juste avant d’analyser. Elle attend l’évènement load de la fenêtre, document.fonts.ready et chaque image encore en cours de chargement (sauf celles en loading="lazy"), pendant au plus timeoutMs (par défaut 5000). Elle ne rejette jamais : quand le temps est écoulé, elle se résout quand même avec ready: false et indique ce qui était encore en attente. Analyser quand même, attendre plus longtemps ou s’arrêter, c’est à vous de choisir ; si vous analysez, conservez avec le résultat ce qui était encore en attente.
const ready = await a11ycore.waitForPageReady({ timeoutMs: 3000 });const result = a11ycore.runa11yCoreInPage(location.href, null, {}, null);
Un driver que vous pilotez vous-même peut l’envoyer dans la page comme runa11yCoreInPage : page.evaluate(waitForPageReady, { timeoutMs: 5000 }). Sous jsdom, il n’y a rien à attendre : elle se résout aussitôt avec ready: true. Les autres options (quietMs, document) et la valeur de retour complète sont dans INTEGRATION.md, le guide d’intégration sur GitHub.
Feuilles de style d’une autre origine
Le script d’une page ne peut pas lire les règles d’une feuille de style servie depuis une autre origine (un CDN, un service de polices), si bien qu’une règle qui cherche quelque chose dans le CSS, comme css-orientation-lock, ne peut pas savoir s’il y est, et pose une question. Donnez leur texte à l’analyse dans engineOptions.styleSheetTexts ([{ href, text }]) et css-orientation-lock, css-hidden-focus et css-focus-indicator-suppressed le lisent. Dans le bundle, runa11yCoreInPageAsync prend les quatre mêmes arguments que runa11yCoreInPage, récupère avec preloadStyleSheets() les feuilles que leur serveur partage (CORS), puis analyse :
const result = await a11ycore.runa11yCoreInPageAsync(location.href, null, {}, null);
Une feuille que son serveur ne partage pas ne peut pas du tout être lue depuis la page. Un outil qui pilote le navigateur peut la récupérer hors de la page, où CORS ne s’applique pas, et la passer dans la même option ; les bindings le font.
Une limite de temps pour une analyse
Une analyse s’exécute d’un seul tenant dans la page, et rien dans la page ne peut l’arrêter une fois lancée : un getter de la page ou une règle qui ne rend jamais la main bloque cet onglet définitivement. Limitez la durée depuis l’endroit où vous lancez l’analyse, et abandonnez l’onglet quand la limite est dépassée : votre limite de temps arrête l’attente, pas le script qui s’exécute dans la page. 60 secondes, c’est bien plus que ce que prend une page normale : seules les pages défaillantes sont donc concernées :
// Playwright or Puppeteer: page.evaluate has no time limit of its own.async function scanWithin(page, ms, scan) {let timer;const late = new Promise((_, reject) => {timer = setTimeout(() => reject(new Error(`the scan took longer than ${ms} ms`)), ms);});try {return await Promise.race([scan(), late]);} catch (err) {// The tab may still be busy with the scan: close it, and use a new one.await page.close().catch(() => {});throw err;} finally {clearTimeout(timer);}}const fs = require('node:fs');await page.evaluate(fs.readFileSync(require.resolve('@surea11y/core/browser'), 'utf8'));const result = await scanWithin(page, 60_000, () =>page.evaluate(() => window.a11ycore.runa11yCoreInPage(location.href, null, {}, null)));
Dans jsdom, l’analyse s’exécute sur le thread même de Node : un minuteur ne peut donc pas s’y déclencher avant qu’elle se termine. Exécutez l’analyse dans un worker thread ou un processus enfant, et arrêtez-le quand la limite est dépassée. La limite de temps propre au job de CI reste le dernier recours dans les deux cas.
Composants web aux internals privés
Un élément personnalisé peut se nommer, fixer ses états et se donner un rôle par ses ElementInternals. Beaucoup gardent leurs internals dans un champ privé, où aucun script ne peut les relire : l’analyse pose donc la question pour un tel élément (cantTell) au lieu de le juger, et ne voit pas du tout un rôle donné ainsi.
internalsHookScript(), de @surea11y/core (pas du bundle), renvoie un script qui enregistre les internals de chaque élément à mesure que la page les attache, pour que l’analyse puisse les lire. Il est facultatif : core ne l’installe jamais. Installez-le avant les scripts de la page, dans chaque frame que vous analysez : avec l’API de scripts d’initialisation de votre driver, ou, sur une page que vous construisez vous-même, comme premier <script> du <head>. Dans un fichier de setup jsdom, qui s’exécute une fois le document chargé mais avant la définition de tout composant, passez { early: true } :
const { internalsHookScript } = require('@surea11y/core');// Playwrightawait page.addInitScript({ content: internalsHookScript() });// Puppeteerawait page.evaluateOnNewDocument(internalsHookScript());// jest.setup.js or vitest.setup.js, before any component is importedwindow.eval(internalsHookScript({ early: true }));
Le résultat indique s’il était là, dans engine.environment.internalsHook : 'early' s’il a été installé avant les scripts de la page, 'late' s’il l’a été après, auquel cas il ne voit que ce que la page attache ensuite. Tout script de la page peut lire ce que le hook enregistre, si bien que les internals privés d’un composant ne le sont plus : installez-le pour des exécutions de test et d’audit, pas dans des pages servies aux utilisateurs. Les autres façons de l’installer, et ce qu’une page peut remarquer, sont dans INTEGRATION.md.
Gestionnaires de clic et régions live
Deux autres hooks facultatifs, depuis core 1.14, s’installent comme le hook d’internals, avant les scripts de la page elle-même. listenerHookScript() enregistre les écouteurs de clic, de clavier, de focus, de souris et de collage qu’une page ajoute depuis un script, pour que les règles de clavier les jugent : un <div role="button"> doté d’un écouteur de clic, sans écouteur de clavier là où va un événement clavier venant de lui, échoue (CLICK_HANDLER_NO_KEY_LISTENER). liveRegionHookScript() enregistre ce que reçoivent les régions live : live-region-announcements pose alors une question sur une région ajoutée avec son texte déjà dedans, ou affichée et remplie d’un coup, que les lecteurs d’écran n’annoncent souvent pas, et getAnnouncements() renvoie ce que la page a mis à disposition pour être annoncé :
const { listenerHookScript, liveRegionHookScript, getAnnouncements } = require('@surea11y/core');// Playwright: before the page's own scriptsawait page.addInitScript({ content: listenerHookScript() });await page.addInitScript({ content: liveRegionHookScript() });await page.goto('https://example.com/settings');await page.click('text=Save');const said = await page.evaluate(getAnnouncements, { selectors: true });// [{ text: 'Saved', politeness: 'polite', time: 1234.5, order: 7, behindModal: false, selector: '#status' }]
engine.environment.listenerHook et .liveRegionHook indiquent si une analyse les a vus. Seul un hook d’écouteurs installé avant les scripts de la page ('early') prouve qu’un écouteur manque ; installé plus tard, les règles ne font que poser une question. Dans le bundle, a11ycore.getAnnouncements() les lit dans la page. Comme le hook d’internals, tous deux gardent ce qu’ils enregistrent là où n’importe quel script de la page peut le lire : installez-les pour les exécutions de test et d’audit. Dans un fichier de configuration jsdom, passez { early: true } et, pour le hook d’écouteurs, exécutez d’abord un sélecteur (document.querySelector('*')), car jsdom ajoute ses propres écouteurs de clavier la première fois qu’un sélecteur s’exécute. Les sections du hook d’écouteurs et du hook de régions live d’INTEGRATION.md, le guide d’intégration sur GitHub, donnent le reste.
Tendances et étapes des questions
Depuis core 1.14, une occurrence cantTell peut porter un lean : le sens dans lequel vont les éléments de la page (toward: 'fail' ou 'pass'), pourquoi, et ce qui pourrait le contredire. Le résultat reste cantTell. Chaque question d’une règle de core porte aussi howToCheck, les étapes qu’une personne suit pour y répondre, dans la langue de l’analyse. Elles sont ajoutées dans Node, pas par le moteur dans la page : runDomRulesInPage les ajoute, et pour un résultat qui revient d’une page, addHowToCheck(result) en renvoie une copie qui les contient. leaningFailures(result) liste les constats qui penchent vers l’échec, pour un seuil à vous :
const { runa11yCoreInPage, addHowToCheck, leaningFailures } = require('@surea11y/core');const raw = await page.evaluate(runa11yCoreInPage, page.url(), null, {}, null);const result = addHowToCheck(raw); // a copy, with each question's stepsconst likelyFailures = leaningFailures(result);
Qu’un constat penche, et ses textes, peuvent changer dans une version mineure de core ; la forme de lean est stable. Voir le schéma de sortie pour ces deux champs.
Savoir ce que couvre une analyse
Un outil qui rapporte par critère WCAG et laisse ses utilisateurs choisir des vérifications peut dire à quelle cible l’analyse correspond : engineOptions.wcagLevel, à côté de wcagVersion, laisse de côté les critères au-dessus de ce niveau sans changer les vérifications exécutées. getRulesForRunOnly(runOnly, engineOptions) liste les critères que rapporte une analyse avec cette sélection, avant de la lancer, avec la logique même de l’analyse :
const { getRulesForRunOnly, runDomRulesInPage } = require('@surea11y/core');const engineOptions = { wcagVersion: '2.2', wcagLevel: 'AA' };const runOnly = { includeRuleIds: ['contrast-computable'] };getRulesForRunOnly(runOnly, engineOptions).map((r) => r.id); // before the scanconst result = runDomRulesInPage(url, null, engineOptions, runOnly);
Un critère dont seules certaines vérifications ont été exécutées revient en cantTell, chaque vérification non exécutée étant marquée notSelected: true : affichez-le comme vérifié en partie, jamais comme réussi. Depuis core 1.14, aucun critère n’est rapporté comme réussi : un critère ne réussit que si ses règles vérifient tout ce qu’il demande, et aucun n’est encore recensé comme entièrement vérifié. Construire votre propre outil sur core (une CLI, une intégration CI, un tableau de bord, un générateur de rapports de conformité) est traité dans BUILDING_ON_CORE.md, sur GitHub : ce sur quoi vous pouvez compter, lancer des analyses, choisir des vérifications, présenter les résultats honnêtement et comparer des exécutions.