スタンドアロンで使う
使いどころ
実際のブラウザーで開いているページの手動チェック、ブックマークレット、そのほか自動化ドライバーもビルド手順も使えない状況に向いています。
使い方
@surea11y/coreはパッケージのルートにsurea11y.browser.jsを同梱しています。Nodeパッケージと同じルールのソースから生成したバンドルです。直接読み込むと、グローバル変数a11ycoreが1つ定義されます。
<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は、各バインディングが内部で呼び出しているのとまったく同じ関数です。このバンドルは、requireやモジュールシステムなしで関数を読み込むためだけにあり、別のAPIを追加するものではありません。引数もほかの場所と同じです。次の例は、1つの領域に絞り、WCAG 2.2のAとAAに限定し、ノイズの多い2つのウィジェットを除外したスキャンです。
<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>
バンドルに含まれるのは英語だけです。ほかのロケールはそれぞれ別ファイルとしてバンドルの横に同梱しています。バンドルの後に読み込むと、その言語が使えるようになります。
<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>
ファイルを読み込んでいないロケールを指定すると、エラーにはならず英語が返ります。ロケールを参照してください。
バンドルはモジュールとしても読み込めます。import a11ycore from '@surea11y/core/browser'、runa11yCoreInPageの名前付きインポート、require()のどれでもそのAPIを得られ、グローバルも引き続き定義されます。
バンドルから意図的に除外しているもの:runa11yCoreAcrossFrames/a11yCoreEnableFrameResponder(フレームをまたぐスキャンには、埋め込まれたフレーム側でもエンジンを読み込んでオプトインする必要があり、scriptタグを1つ置くだけの使い方には合いません)。必要な場合はnpmパッケージを直接使ってください。INTEGRATION.mdを参照してください。
バインディングを使わずにrequire('@surea11y/core')を直接呼び出すのが、もう1つのスタンドアロンな方法です。INTEGRATION.mdのPattern 1/Pattern 2にあたります。実際のブラウザーなしでNode上のjsdomを使う方法と、バインディングを経由せずにPuppeteer/Playwrightを自分で操作する方法です。テストでは、@surea11y/core/testingのruna11yCoreOnHtml(html, options)が、ドキュメントを用意したうえでHTML文字列をjsdomでスキャンします。core 1.12からは、そのcontentTypeオプションで、SVGやXHTMLのドキュメントをその形式どおりに解析できます。
ブラウザーのコンソールから
手作業でスキャンするために、バンドルにはいくつかの補助関数があります。ブラウザーのコンソール(開発者ツールのConsoleタブ)から開いているページにバンドルを読み込み、スキャン、検出結果の一覧表示、ページ上での枠表示、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)はエンジンのオプションに加えて、独自のcontext(ページの一部だけをスキャンするセレクター)とrunOnlyを受け取り、スキャンの前に、ページ自身が読めないスタイルシートを取得します。table()は検出結果を1行ずつ表示します。まず失敗、次に不適合に傾いた問い、適合に傾いた問い、人だけが判断できる問いの順です。highlight()は、fail(赤の実線)やcantTell(琥珀色の破線)が示す各要素に枠を描きます。枠は専用のレイヤーに描かれ、ページの要素には手を加えません。clearHighlights()で消せます。inspect()は、ページを確認する人が手作業でチェックする内容を描くだけで、何も判定しません('headings'、'landmarks'、'names'、'focus-order')。openReport()は初回にバンドルの隣からsurea11y.report.jsを取得し、レポートには各問いの確認方法が示されます。ページのContent Security PolicyがCDNのスクリプトを遮断する場合は、ページ自身のオリジンからファイルを読み込むか、ドライバーでスキャンしてください。すべてのオプションは、GitHubにある連携ガイドINTEGRATION.mdのブラウザーのコンソールの節にあります。
ページ内でのパック
パックはNodeで用意されるため、スクリプトとしてページに届きます。スクリプトをファイルとして読み込むページ(<script>タグ、ブックマークレット、拡張機能)では、Nodeで1つのファイルを作ってください。@surea11y/core/packのbuildBrowserBundle({ packs })は、バンドルの後にパックを登録したファイルを作ります。そのうえで、engineOptions.packsでパックをname@versionの形で指定してください。ルールのコードは関数としてスクリプトに書き込まれるため、コンテンツセキュリティポリシーが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>
パックのスクリプトは、それを書き出したコアでしか動作しません。コアとパックが合わせて作るカタログを、Nodeで検証したうえで含んでいるためです。バンドルが別のバージョンのコアであるページは、パックなしでスキャンを実行し、各パックを2つのバージョンとともにskippedPacksに列挙します。1つのファイルに両方が入っていれば、ずれることはありません。コアやパックを更新したら作り直し、ページが指定するname@versionも更新してください。
コアのバンドルがすでにページにある場合は、同じコアでpackScript(packs)をファイルに書き出し、バンドルの後に(スキャンするすべてのフレームにも)読み込んでください。
// 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>
ページが実行するのは、1回のpackScriptの呼び出しが登録したとおりのパックのセットで、そのスクリプトを用意したときと同じコアで実行されます。スキャンに必要なパックはすべて1回の呼び出しに渡してください。指定されてもページが実行しないパックは、理由とともにskippedPacksに列挙されます。バインディングでは、.withPacks()がこれをすべて行います。
スキャン前にページの準備を待つ
スキャンは、その時点のページの状態を読み取ります。開いたばかりのページでは、Webフォントがまだ読み込み中だったり、画像が届いていなかったりするため、同じページでも1秒後にスキャンすると別の結果になることがあります。スキャンはこの状態をengine.environment.fontsと.imagesに記録します(出力スキーマを参照)。ただし、より良い方法は、ページが落ち着くまでスキャンしないことです。
その待機を行うのがwaitForPageReady(options)で、バンドルでは同じグローバルに用意されています。スキャンがこれを自動で呼ぶことはないので、スキャンの直前に呼び出してください。ウィンドウのloadイベント、document.fonts.ready、読み込み中のすべての画像(loading="lazy"のものを除く)を、最大timeoutMs(デフォルトは5000)まで待ちます。rejectすることはありません。時間切れになってもresolveし、ready: falseとともに、まだ完了していなかったものを示します。それでもスキャンするか、もっと待つか、やめるかは、あなたが決めることです。スキャンする場合は、未完了だったものを結果と一緒に残してください。
const ready = await a11ycore.waitForPageReady({ timeoutMs: 3000 });const result = a11ycore.runa11yCoreInPage(location.href, null, {}, null);
自分で操作するドライバーからは、runa11yCoreInPageと同じ方法でページに送り込めます(page.evaluate(waitForPageReady, { timeoutMs: 5000 }))。jsdomでは待つものがないため、すぐにready: trueでresolveします。そのほかのオプション(quietMs、document)と戻り値の詳細は、GitHub上の統合ガイドINTEGRATION.mdにあります。
別オリジンのスタイルシート
ページのスクリプトは、別オリジン(CDNやフォントサービス)から配信されたスタイルシートのルールを読めません。そのため、css-orientation-lockのようにCSSの中の何かを探すルールは、それがあるかどうかを判断できず、確認を求めます。engineOptions.styleSheetTexts([{ href, text }])でスキャンにそのテキストを渡すと、css-orientation-lock、css-hidden-focus、css-focus-indicator-suppressedがそれを読みます。バンドルでは、runa11yCoreInPageAsyncがruna11yCoreInPageと同じ4つの引数を受け取り、サーバーが共有している(CORS)スタイルシートをpreloadStyleSheets()で取得してからスキャンします。
const result = await a11ycore.runa11yCoreInPageAsync(location.href, null, {}, null);
サーバーが共有していないスタイルシートは、ページからはまったく読めません。ブラウザーを操作するツールなら、CORSが適用されないページの外から取得して、同じオプションで渡せます。バインディングはそうしています。
スキャンの制限時間
スキャンはページ内で一度に実行され、いったん始まるとページ内のどれもそれを止められません。戻らないページのゲッターやルールは、そのタブを止めたままにします。スキャンを開始する側で時間を制限し、制限を過ぎたらそのタブを諦めてください。時間制限で止まるのは待機であり、ページ内で実行中のスクリプトではありません。60秒は通常のページにかかる時間をはるかに上回るので、引っかかるのは壊れたページだけです。
// 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)));
jsdomではスキャンがNode自身のスレッドで実行されるため、そこでのタイマーはスキャンが終わるまで発火できません。スキャンをワーカースレッドか子プロセスで実行し、制限を過ぎたらそれを終了してください。どちらの場合も、最後の手段はCIジョブ自体の制限時間です。
非公開のinternalsを持つWebコンポーネント
カスタム要素は、ElementInternalsを通じて名前を付けたり、状態を設定したり、ロールを与えたりできます。多くのコンポーネントはそれをプライベートフィールドに保持しており、スクリプトからは読み返せません。そのためスキャンはそうした要素を判定せずに問いかけ(cantTell)、このように設定されたロールはまったく認識しません。
@surea11y/coreのinternalsHookScript()(バンドルには含まれません)は、ページが取り付ける各要素のinternalsを記録するスクリプトを返し、スキャンがそれを読めるようにします。オプトインで、coreが自動でインストールすることはありません。スキャンするすべてのフレームで、ページ自身のスクリプトより前にインストールしてください。ドライバーの初期化スクリプト用APIを使うか、自分で作るページなら<head>の最初の<script>として置きます。jsdomのセットアップファイルは、ドキュメントの読み込み後、どのコンポーネントも定義される前に実行されるので、{ 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 }));
フックがあったかどうかは、結果のengine.environment.internalsHookでわかります。ページのスクリプトより前にインストールされた場合は'early'、後からインストールされ、それ以降にページが取り付けたものしか見ていない場合は'late'です。フックが記録したものはページ内のどのスクリプトからも読めるため、コンポーネントの非公開のinternalsは非公開でなくなります。テストや監査のための実行でだけインストールし、利用者に配信するページでは使わないでください。そのほかのインストール方法や、ページから何が見えるかは、INTEGRATION.mdにあります。
クリックハンドラーとライブリージョン
core 1.14から、オプトインのフックがさらに2つあり、internalsフックと同じように、ページ自身のスクリプトより前にインストールします。listenerHookScript()は、ページがスクリプトから追加するクリック、キー、フォーカス、マウス、貼り付けのリスナーを記録し、キーボードのルールがそれを判定できるようにします。クリックリスナーを持ち、そこからのキーイベントが届く先のどこにもキーリスナーがない<div role="button">は失敗します(CLICK_HANDLER_NO_KEY_LISTENER)。liveRegionHookScript()は、ライブリージョンに何が与えられたかを記録します。するとlive-region-announcementsは、テキストが入った状態で追加されたリージョンや、表示と同時に中身が入ったリージョンなど、スクリーンリーダーが読み上げないことの多いものについて確認を求め、getAnnouncements()はページが読み上げ用に提供した内容を返します。
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と.liveRegionHookは、スキャンがフックを確認できたかを示します。リスナーがないことを示せるのは、ページのスクリプトより前にインストールされたリスナーフック('early')だけで、後からインストールした場合、ルールは確認を求めるだけです。バンドルではa11ycore.getAnnouncements()がページ内で記録を読みます。internalsフックと同じく、どちらも記録した内容をページのどのスクリプトからも読める場所に保持するので、テストや監査の実行のためにインストールしてください。jsdomのセットアップファイルでは{ early: true }を渡し、リスナーフックの前にセレクターを1回実行してください(document.querySelector('*'))。jsdomは最初にセレクターを実行したときに独自のキーリスナーを追加するためです。残りは、GitHubにある連携ガイドINTEGRATION.mdのリスナーフックとライブリージョンフックの節にあります。
問いの傾きと確認方法
core 1.14から、cantTellの出現箇所はleanを持つことがあります。ページの証拠がどちらを示しているか(toward: 'fail'または'pass')、その理由、それが誤りになりうる点です。判定結果はcantTellのままです。coreのルールの各問いには、人が答えるための手順であるhowToCheckも、スキャンのロケールで含まれます。これはページ内のエンジンではなくNodeで追加されます。runDomRulesInPageは自動で追加し、ページから返ってきた結果にはaddHowToCheck(result)が手順を加えたコピーを返します。leaningFailures(result)は、独自のゲートのために、不適合に傾いた検出結果を一覧にします。
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);
検出結果が傾くかどうかとその文言はcoreのマイナーリリースで変わることがありますが、leanの形は安定しています。両方のフィールドについては出力スキーマを参照してください。
スキャンの対象を事前に知る
WCAGの達成基準ごとに報告し、利用者にチェックを選ばせるツールでは、スキャンの目標を指定できます。wcagVersionと並べて指定するengineOptions.wcagLevelは、実行するチェックを変えずに、そのレベルを超える達成基準を報告から外します。getRulesForRunOnly(runOnly, engineOptions)は、その選択でスキャンが報告する達成基準を、実行する前に、スキャン自身のロジックで一覧にします。
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);
チェックの一部しか実行されなかった達成基準はcantTellとして返り、実行されなかった各チェックにはnotSelected: trueが付きます。こうした達成基準は「一部だけチェック済み」として示し、合格とは決して表示しないでください。core 1.14からは、合格と報告される達成基準はそもそもありません。達成基準が合格になるのは、そのルールが基準の求めることをすべてチェックする場合だけで、完全にチェックできるとされた基準はまだないためです。core上に自分のツール(CLI、CI連携、ダッシュボード、適合性レポートの生成ツールなど)を作る方法は、GitHubのBUILDING_ON_CORE.mdにあります。頼れる契約、スキャンの実行、チェックの選び方、結果を誠実に示す方法、実行結果の比較を扱っています。