Standalone-Verwendung
Wann Sie es verwenden
Für eine manuelle Prüfung einer Seite, die in einem echten Browser geöffnet ist, für ein Bookmarklet oder für jeden anderen Kontext ohne Automatisierungstreiber und ohne Build-Schritt.
Verwendung
@surea11y/core liefert im Paketstamm surea11y.browser.js mit, ein Bundle, das aus denselben Regelquellen erzeugt wird wie das Node-Paket. Wenn Sie es direkt laden, definiert es eine globale Variable, 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 ist genau dieselbe Funktion, die die Bindings intern aufrufen — das Bundle existiert nur, um das Laden ohne require/Modulsystem zu lösen, nicht um eine separate API anzubieten. Die Argumente sind dieselben wie überall sonst. Ein Scan, der auf einen Bereich und auf WCAG 2.2 A und AA beschränkt ist und zwei störende Widgets auslässt:
<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>
Das Bundle enthält nur Englisch. Jede andere Locale wird daneben als eigene Datei ausgeliefert — laden Sie eine nach dem Bundle, und diese Sprache steht zur Verfügung:
<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>
Wenn Sie eine Locale anfordern, deren Datei Sie nicht geladen haben, erhalten Sie Englisch, keinen Fehler — siehe Locales.
Das Bundle lässt sich auch als Modul laden: import a11ycore from '@surea11y/core/browser', ein benannter Import von runa11yCoreInPage und require() erhalten alle seine API, und das Global ist weiterhin definiert.
Bewusst nicht im Bundle enthalten: runa11yCoreAcrossFrames/a11yCoreEnableFrameResponder (frameübergreifendes Scannen setzt voraus, dass auch der eingebettete Frame die Engine lädt und dies ausdrücklich aktiviert — das passt nicht zu einem einzelnen eingefügten Script-Tag). Verwenden Sie direkt das npm-Paket, wenn Sie das brauchen — siehe INTEGRATION.md.
require('@surea11y/core') direkt, ohne Binding, ist der andere Standalone-Weg — das entspricht Pattern 1/Pattern 2 in INTEGRATION.md: jsdom in Node ohne echten Browser, oder Puppeteer/Playwright selbst steuern, statt über ein Binding zu gehen. Für Tests scannt runa11yCoreOnHtml(html, options) aus @surea11y/core/testing einen HTML-String in jsdom und richtet das Dokument für Sie ein; seit core 1.12 liest seine Option contentType ein SVG- oder XHTML-Dokument als das, was es ist.
Aus der Browser-Konsole
Für einen Scan von Hand hat das Bundle einige Helfer. Laden Sie es über die Konsole des Browsers (den Tab „Console“ der Entwicklertools) in eine beliebige geöffnete Seite, und scannen Sie dann, listen Sie die Befunde auf, markieren Sie sie auf der Seite oder öffnen Sie den HTML-Bericht:
// 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) nimmt die Engine-Optionen und zwei eigene, context (ein Selektor, um nur einen Teil der Seite zu scannen) und runOnly, und ruft vor dem Scan die Stylesheets ab, die die Seite selbst nicht lesen kann. table() gibt eine Zeile pro Befund aus: zuerst die Fehlschläge, dann die Fragen, die zu fail tendieren, die, die zu pass tendieren, und die, die nur eine Person beurteilen kann. highlight() zeichnet einen Rahmen um jedes Element, das ein fail (durchgehend rot) oder ein cantTell (gestrichelt bernsteinfarben) nennt, in einer eigenen Ebene, die die Elemente der Seite unverändert lässt; clearHighlights() entfernt sie. inspect() zeichnet, was eine prüfende Person von Hand kontrolliert, und beurteilt nichts: 'headings', 'landmarks', 'names' oder 'focus-order'. openReport() lädt beim ersten Mal surea11y.report.js von neben dem Bundle, und der Bericht zeigt, wie sich jede Frage prüfen lässt. Die Content Security Policy einer Seite kann ein Skript von einem CDN blockieren: Laden Sie die Dateien dann vom eigenen Origin der Seite, oder scannen Sie mit einem Driver. Alle Optionen stehen im Abschnitt zur Browser-Konsole in INTEGRATION.md, dem Integrationsleitfaden auf GitHub.
Packs in einer Seite
Ein Pack wird in Node vorbereitet und gelangt daher als Script in eine Seite. Für eine Seite, die ihre Scripts als Dateien lädt (ein <script>-Tag, ein Bookmarklet, eine Erweiterung), bauen Sie in Node eine einzige Datei: buildBrowserBundle({ packs }) aus @surea11y/core/pack ist das Bundle, nach dem die Packs registriert werden. Nennen Sie die Packs dann in engineOptions.packs als name@version. Der Code der Regeln wird als Funktionen in das Script geschrieben, daher läuft er auch dort, wo eine Content Security Policy eval verbietet.
// 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>
Das Script eines Packs läuft nur mit dem core, der es geschrieben hat: Es enthält den Katalog, den core und die Packs zusammen bilden, in Node geprüft. Eine Seite, deren Bundle eine andere Version von core ist, führt den Scan ohne die Packs aus und führt jedes in skippedPacks auf, mit beiden Versionen. Eine einzige Datei enthält beides, also können sie nicht auseinanderlaufen. Bauen Sie sie neu, wenn Sie core oder ein Pack aktualisieren, und passen Sie das name@version an, das die Seite nennt.
Ist das Bundle von core bereits in der Seite, schreiben Sie packScript(packs) mit demselben core in eine Datei und laden Sie sie nach dem Bundle (und in jeden Frame, den Sie scannen):
// 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>
Die Seite führt genau die Packs aus, die ein Aufruf von packScript registriert hat, mit dem core, mit dem er vorbereitet wurde: Übergeben Sie jedes Pack, das ein Scan braucht, an einen einzigen Aufruf. Genannte Packs, die die Seite nicht ausführt, stehen mit dem Grund in skippedPacks. Die Bindings erledigen all das mit .withPacks() für Sie.
Vor dem Scan auf die Seite warten
Ein Scan liest die Seite so, wie sie in diesem Moment ist. Auf einer gerade geöffneten Seite laden Webfonts womöglich noch, und Bilder sind vielleicht noch nicht da; dieselbe Seite kann eine Sekunde später ein anderes Ergebnis liefern. Der Scan hält das in engine.environment.fonts und .images fest (siehe das Ausgabeschema), besser ist es aber, erst zu scannen, wenn die Seite fertig geladen ist.
waitForPageReady(options) übernimmt dieses Warten, und das Bundle stellt es auf demselben Global bereit. Der Scan ruft es nie selbst auf; rufen Sie es direkt vor dem Scan auf. Es wartet auf das load-Event des Fensters, auf document.fonts.ready und auf jedes Bild, das noch lädt (außer solchen mit loading="lazy"), höchstens timeoutMs lang (Standard 5000). Es lehnt nie ab: Läuft die Zeit ab, löst es trotzdem auf, mit ready: false, und nennt, was noch ausstand. Ob Sie trotzdem scannen, länger warten oder abbrechen, entscheiden Sie; wenn Sie scannen, bewahren Sie mit dem Ergebnis auf, was noch ausstand.
const ready = await a11ycore.waitForPageReady({ timeoutMs: 3000 });const result = a11ycore.runa11yCoreInPage(location.href, null, {}, null);
Ein Treiber, den Sie selbst steuern, kann es genauso in die Seite schicken wie runa11yCoreInPage: page.evaluate(waitForPageReady, { timeoutMs: 5000 }). In jsdom gibt es nichts zu warten, es löst also sofort mit ready: true auf. Die übrigen Optionen (quietMs, document) und der vollständige Rückgabewert stehen in INTEGRATION.md, der Integrationsanleitung auf GitHub.
Stylesheets von einem anderen Origin
Das Skript einer Seite kann die Regeln eines Stylesheets, das von einem anderen Origin kommt (einem CDN, einem Schriftdienst), nicht lesen. Eine Regel, die im CSS nach etwas sucht, etwa css-orientation-lock, kann daher nicht sagen, ob es da ist, und stellt eine Frage. Geben Sie dem Scan ihren Text in engineOptions.styleSheetTexts ([{ href, text }]), und css-orientation-lock, css-hidden-focus und css-focus-indicator-suppressed lesen ihn. Im Bundle nimmt runa11yCoreInPageAsync dieselben vier Argumente wie runa11yCoreInPage, ruft mit preloadStyleSheets() die Stylesheets ab, die ihr Server freigibt (CORS), und scannt dann:
const result = await a11ycore.runa11yCoreInPageAsync(location.href, null, {}, null);
Ein Stylesheet, das sein Server nicht freigibt, lässt sich aus der Seite gar nicht lesen. Ein Werkzeug, das den Browser steuert, kann es außerhalb der Seite abrufen, wo kein CORS gilt, und in derselben Option übergeben; die Bindings tun das.
Ein Zeitlimit für einen Scan
Ein Scan läuft in einem Zug innerhalb der Seite, und nichts in der Seite kann ihn anhalten, sobald er begonnen hat: Ein Getter der Seite oder eine Regel, die nie zurückkehrt, blockiert diesen Tab endgültig. Begrenzen Sie die Zeit dort, wo Sie den Scan starten, und geben Sie den Tab auf, wenn das Limit überschritten ist: Ihr Zeitlimit beendet das Warten, nicht das Skript, das in der Seite läuft. 60 Sekunden liegen weit über dem, was eine normale Seite braucht, und erfassen daher nur die defekten:
// 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)));
In jsdom läuft der Scan auf dem eigenen Thread von Node, daher kann ein Timer dort erst auslösen, wenn er endet: Führen Sie den Scan in einem Worker-Thread oder einem Kindprozess aus, und beenden Sie diesen, wenn das Limit überschritten ist. Das Zeitlimit des CI-Jobs selbst ist in beiden Fällen der letzte Ausweg.
Web Components mit privaten Internals
Ein Custom Element kann sich über ElementInternals einen Namen geben, seine Zustände setzen und sich eine Rolle geben. Viele halten ihre Internals in einem privaten Feld, wo kein Skript sie zurücklesen kann. Ein Scan fragt deshalb bei einem solchen Element nach (cantTell), statt es zu beurteilen, und sieht eine so gesetzte Rolle gar nicht.
internalsHookScript() aus @surea11y/core (nicht aus dem Bundle) gibt ein Skript zurück, das die Internals jedes Elements erfasst, sobald die Seite sie anhängt, damit der Scan sie lesen kann. Es ist optional: Core installiert es nie. Installieren Sie es vor den eigenen Skripten der Seite, in jedem Frame, den Sie scannen: mit der Init-Script-API Ihres Treibers oder, auf einer Seite, die Sie selbst bauen, als erstes <script> im <head>. In einer jsdom-Setup-Datei, die läuft, nachdem das Dokument geladen ist, aber bevor eine Komponente definiert wird, übergeben Sie { 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 }));
Ob der Hook da war, zeigt das Ergebnis in engine.environment.internalsHook: 'early', wenn er vor den Skripten der Seite installiert wurde, 'late', wenn er danach kam und nur sieht, was die Seite ab dann anhängt. Jedes Skript der Seite kann lesen, was der Hook erfasst, private Internals einer Komponente sind damit nicht mehr privat: Installieren Sie ihn für Test- und Audit-Läufe, nicht in Seiten, die Sie an Nutzende ausliefern. Alle anderen Wege, ihn zu installieren, und was eine Seite davon bemerken kann, stehen in INTEGRATION.md.
Klick-Handler und Live-Regionen
Zwei weitere optionale Hooks, seit core 1.14, werden wie der Internals-Hook installiert, vor den eigenen Skripten der Seite. listenerHookScript() zeichnet die Klick-, Tastatur-, Fokus-, Maus- und Einfüge-Listener auf, die eine Seite per Skript hinzufügt, damit die Tastaturregeln sie beurteilen: Ein <div role="button"> mit einem Klick-Listener und ohne Tastatur-Listener überall dort, wohin ein Tastaturereignis von ihm gelangt, ergibt fail (CLICK_HANDLER_NO_KEY_LISTENER). liveRegionHookScript() zeichnet auf, was Live-Regionen erhalten: live-region-announcements stellt dann eine Frage zu einer Region, die schon mit ihrem Text eingefügt oder auf einmal eingeblendet und gefüllt wurde und die Screenreader oft nicht ansagen, und getAnnouncements() gibt zurück, was die Seite zur Ansage bereitgestellt hat:
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 und .liveRegionHook zeigen, ob ein Scan sie gesehen hat. Nur ein Listener-Hook, der vor den Skripten der Seite installiert wurde ('early'), belegt, dass ein Listener fehlt; später installiert, stellen die Regeln nur eine Frage. Im Bundle liest a11ycore.getAnnouncements() sie in der Seite. Wie der Internals-Hook legen beide das, was sie aufzeichnen, dort ab, wo jedes Skript der Seite es lesen kann: Installieren Sie sie für Test- und Audit-Läufe. In einer jsdom-Setup-Datei übergeben Sie { early: true } und führen für den Listener-Hook vorher einen Selektor aus (document.querySelector('*')), weil jsdom beim ersten Selektor eigene Tastatur-Listener hinzufügt. Die Abschnitte zum Listener-Hook und zum Live-Region-Hook in INTEGRATION.md, dem Integrationsleitfaden auf GitHub, enthalten den Rest.
Tendenzen und Prüfschritte für Fragen
Seit core 1.14 kann ein cantTell-Vorkommen einen lean enthalten: in welche Richtung die Hinweise auf der Seite zeigen (toward: 'fail' oder 'pass'), warum, und was dagegen sprechen könnte. Das Ergebnis bleibt cantTell. Jede Frage einer core-Regel enthält außerdem howToCheck, die Schritte, mit denen eine Person sie beantwortet, in der Locale des Scans. Sie werden in Node hinzugefügt, nicht von der Engine in der Seite: runDomRulesInPage fügt sie hinzu, und für ein Ergebnis, das aus einer Seite zurückkommt, gibt addHowToCheck(result) eine Kopie mit ihnen zurück. leaningFailures(result) listet die Befunde, die zu fail tendieren, für eine eigene Schwelle:
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);
Ob ein Befund tendiert, und seine Texte, können sich in einer Minor-Version von core ändern; die Form von lean ist stabil. Beide Felder beschreibt das Ausgabeschema.
Wissen, was ein Scan abdeckt
Ein Werkzeug, das nach WCAG-Kriterium berichtet und seine Nutzenden Prüfungen auswählen lässt, kann angeben, für welches Ziel der Scan gilt: engineOptions.wcagLevel neben wcagVersion lässt Kriterien oberhalb dieser Stufe aus, ohne zu ändern, welche Prüfungen laufen. getRulesForRunOnly(runOnly, engineOptions) listet die Kriterien auf, die ein Scan mit dieser Auswahl meldet, bevor Sie ihn ausführen, mit der Logik des Scans selbst:
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);
Ein Kriterium, von dessen Prüfungen nur einige liefen, kommt als cantTell zurück, und jede Prüfung, die nicht lief, ist mit notSelected: true markiert: Zeigen Sie es als teilweise geprüft, nie als bestanden. Seit core 1.14 wird überhaupt kein Kriterium als bestanden gemeldet: Ein Kriterium gilt nur dann als bestanden, wenn seine Regeln alles prüfen, was es verlangt, und noch keines ist als vollständig geprüft erfasst. Wie Sie ein eigenes Werkzeug auf core aufbauen (eine CLI, eine CI-Integration, ein Dashboard, einen Generator für Konformitätsberichte), beschreibt BUILDING_ON_CORE.md auf GitHub: worauf Sie sich verlassen können, Scans ausführen, Prüfungen auswählen, Ergebnisse ehrlich darstellen und Läufe vergleichen.