HTML-in-Canvas API 徹底解説 — Canvas に本物の DOM を描く新仕様と、Flutter Web が抱える同じ課題

はじめに

<canvas> に描いた文字は、ブラウザにとって文字ではない。ピクセルの集合でしかないため、選択もコピーも検索もできず、スクリーンリーダーからは何も読み上げられない。この制約は Canvas API が登場してから 20 年近く変わらないまま、WebGL のゲーム UI、データビジュアライゼーション、地図アプリ、そして Flutter Web のようなキャンバス描画型フレームワークにのしかかってきた。

Google I/O 2026 で発表された HTML-in-Canvas API は、この前提を根本から変えようとしている。Canvas の中に「実在する DOM 要素」を配置し、それをそのまま描画する。描かれるのは画像ではなく、アクセシビリティツリーに乗った本物の要素だ。この記事では API の設計、WebGL/WebGPU への波及、あえて描画されないものの一覧が意味するもの、そして Flutter Web が抱える同型の課題との関係を整理する。

Canvas の中のテキストは、どこにも存在しない

まず、この API が解こうとしている問題の大きさを確認しておきたい。

ctx.fillText() には改行がない。折り返しもない。複数行のテキストを Canvas に描くには、文字幅を measureText() で測りながら自前で行分割アルゴリズムを書くことになる。日本語の禁則処理、アラビア語やヘブライ語の双方向テキスト、タイ語の単語境界、ルビ — CSS が何十年もかけて解決してきたレイアウト処理を、全部もう一度実装する羽目になる。

UI 要素はさらに深刻だ。Canvas 上にボタンを置きたければ、矩形を描き、マウス座標との当たり判定を書き、ホバー状態を管理し、フォーカスリングを描き、Tab キーによるフォーカス移動順序を実装し、Enter/Space での発火を処理し、そのうえで ARIA 相当の情報をどこかに用意しなければならない。ネイティブの <button> が無償で提供している挙動の再実装であり、しかも大半のプロジェクトでは中途半端なところで力尽きる。

そして最後に残るのがアクセシビリティだ。Canvas はアクセシビリティツリーに対して「画像 1 枚」としてしか見えない。aria-label で説明を補うことはできても、内部の構造は伝わらない。ページ内検索も効かず、翻訳機能も働かず、ブラウザ拡張も干渉できず、DevTools の Elements パネルには何も映らない。

3 つのプリミティブ — API はどう動くか

HTML-in-Canvas の設計は驚くほど小さい。W3C の WICG で議論されている提案は、本質的に 3 つのプリミティブだけで構成されている。

1. layoutsubtree 属性 — <canvas> に付ける opt-in フラグ。これを付けると、Canvas の子要素が「フォールバックコンテンツ」ではなく「レイアウトとヒットテストの対象」として扱われるようになる。従来の Canvas では子要素は非対応ブラウザ向けの代替表示でしかなかったが、その意味が切り替わる。

2. 描画メソッド — 子要素を各レンダリングコンテキストに描き込む。

3. paint イベント — 子要素の描画内容が変化したときに発火し、再描画のタイミングを教えてくれる。changedElements で何が変わったかを取得できる。

最小構成はこうなる。

<canvas id="canvas" width="800" height="600" layoutsubtree>
  <div id="content">
    <h2>本物の見出し</h2>
    <p>折り返しも、双方向テキストも、CSS がやってくれる。</p>
    <button>本物のボタン</button>
  </div>
</canvas>
const canvas = document.getElementById("canvas");
const content = document.getElementById("content");
const ctx = canvas.getContext("2d");

ctx.drawElementImage(content, 0, 0);

drawElementImage(element, x, y) は、指定した子要素を現在の変換行列を適用したうえで描画する。ここで重要なのは戻り値だ。適用された変換を DOMMatrix として返す。

const transform = ctx.drawElementImage(content, 0, 0);
content.style.transform = transform.toString();

この 2 行が API の肝にあたる。Canvas 側で translate() や rotate() や scale() を掛けて要素を描画したあと、同じ変換を裏側の実 DOM 要素にも適用する。これで見た目の位置と、クリック判定・テキスト選択・フォーカスの位置が一致する。ユーザーは Canvas の絵をクリックしているつもりで、実際には裏にある本物の <button> を押している。

継続的に更新する場合は paint イベントを使う。

canvas.addEventListener("paint", () => {
  ctx.reset();
  ctx.drawElementImage(content, 0, 0);
});
canvas.requestPaint();

requestAnimationFrame で毎フレーム描き直すのではなく、「中身が変わったときだけ」ブラウザが教えてくれる。テキスト入力中のキャレット点滅や、CSS アニメーションの進行も、このイベント経由で拾える設計になっている。

2D だけの話ではない — WebGL と WebGPU への波及

この API が単なる「Canvas のテキスト描画改善」に留まらないのは、3 つのレンダリングコンテキストすべてに入るからだ。

コンテキストメソッド
2Dctx.drawElementImage(element, x, y)
WebGLgl.texElementImage2D(target, level, internalFormat, format, type, element)
WebGPUcopyElementImageToTexture(element, { texture })

WebGL の texElementImage2D は、既存の texImage2D の並びに置かれている。つまり DOM 要素をテクスチャとして GPU に転送できる。3D 空間に浮かぶパネルに、本物の HTML フォームを貼り付けられるということだ。

これまで WebGL/WebGPU アプリで「3D 空間内に UI を置く」には、大きく 2 つの手しかなかった。DOM を CSS の transform で 3D 風に重ねる(CSS3DRenderer 系。深度のオクルージョンが効かない)か、html2canvas などで DOM をラスタライズしてテクスチャにする(重い、CSS 再現度が低い、インタラクションが死ぬ)か。HTML-in-Canvas は 3 つ目の道になる。

ただし WebGL/WebGPU 側では座標変換を自前で処理する必要がある。モデルビュープロジェクション行列をクリップ空間から CSS ピクセル空間へ変換し直すのだが、Chrome の解説記事に載っている実装は素直に読むと面食らう分量だ。

const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));
const width = targetHTMLElement.offsetWidth;
const height = targetHTMLElement.offsetHeight;
const cssToUnitSpace = new DOMMatrix()
  .scale(1 / width, -1 / height, 1)
  .translate(-width / 2, -height / 2);
const clipToCanvasViewport = new DOMMatrix()
  .translate(canvas.width / 2, canvas.height / 2)
  .scale(canvas.width / 2, -canvas.height / 2, 1);
const screenSpaceTransform = clipToCanvasViewport
  .multiply(mvpDOM)
  .multiply(cssToUnitSpace);

const computedTransform = canvas.getElementTransform(
  targetHTMLElement,
  screenSpaceTransform
);
targetHTMLElement.style.transform = computedTransform.toString();

Y 軸の反転が 2 回入り、単位空間とビューポート空間の往復がある。この手の行列コードは一度書けば動くが、デバッグは苦しい。幸い Three.js は HTMLTexture、PlayCanvas は htmlTexture という抽象を用意しており、ライブラリ経由なら 3 行で済む見込みだ。生の WebGL で書く必要は実質ないと考えてよい。

「描かれないもの」リストが語る設計思想

この API で最も読む価値があるのは、機能一覧ではなく除外リストのほうだ。以下は Canvas に描画されない。

  • IME の未確定文字列の下線・装飾
  • スペルチェックの波線
  • :visited による訪問済みリンクの色
  • クロスオリジン iframe の中身
  • 字幕・キャプションのユーザー表示設定

この並びには一貫した理由がある。すべて「ユーザー固有の状態」であり、getImageData() でピクセルを読み戻せる Canvas に描いてしまうと、JavaScript から間接的に読み取れてしまう。

訪問済みリンクの色は典型例だ。CSS の :visited は 2010 年代前半に大規模な履歴漏洩の経路として塞がれ、getComputedStyle からも読めなくなっている。同じ穴を Canvas 経由で開け直すわけにはいかない。IME の未確定文字列も同様で、変換途中の文字列がピクセルとして読み出せれば、実質的にキーロガーの材料になる。

とはいえ、これで問題が消えたわけではない。Chromium の Intent to Experiment では、フォームコントロールのネイティブ描画やグラデーションのピクセル値から「少量の新しい情報」が露出しうるとリスクが明記されている。OS のテーマ、フォントレンダリング設定、アクセントカラー — これらは Canvas フィンガープリンティングの既知の材料であり、ネイティブ UI をピクセル化できるようになれば材料は増える。

Mozilla はこの点を継続的な懸念として挙げている。標準化ポジションの正式表明は出ていない(mozilla/standards-positions の Issue #1076 は 2024 年 9 月の起票以来 “Needs proposed position” のまま)が、blink-dev のスレッドでは Gecko のシグナルは “No Signal” として扱われ、仕様の stage 2 進行自体には反対しない一方で、フィンガープリンティングとウェブ互換性の懸念は残るという立場が記録されている。Firefox は元々 Canvas 読み戻しにノイズを混入させる対策を入れているため、この API が入るとしても Firefox 上での挙動は他ブラウザと一致しない可能性がある。

WebKit については、2026 年 7 月から試験的な実装が始まったと報じられているが、正式なポジションは未表明だ。

Flutter Web は、同じ問題をもう一段深く抱えている

ここが、この API の話をフロントエンド以外のエンジニアも追っておくべき理由になる。

Flutter Web は CanvasKit(Skia の WASM ビルド)または skwasm レンダラで UI を描く。つまりアプリ全体が 1 枚の Canvasだ。当然、上で書いた問題を全部踏む。そこで Flutter は独自の解を持ち込んでいる。フレームワーク内部の Semantics ツリーを、<flt-semantics-host> / <flt-semantics> / <flt-semantics-container> というカスタム要素の DOM ツリーに変換し、Canvas に重ねて配置する。各要素には role、aria-label、tabindex、イベントリスナが付与され、スクリーンリーダーには「ボタン」として見える。実体は <button> ではないが、ARIA で振る舞いを宣言している。

構造としては HTML-in-Canvas とよく似ている。ピクセルの裏に、意味を持った DOM を並行して維持するという発想は同じだ。違いは誰がそれを維持するかで、Flutter ではフレームワークが全部背負っている。結果として、スクリーンリーダーごとの挙動差、ウィジェットによっては semantics が正しく出ない、といった不整合が 2026 年時点でも Flutter Web の最大のギャップとして残り続けている。

HTML-in-Canvas がブラウザに入ると、この構図が変わりうる。アクセシビリティツリーへの露出をブラウザ自身が保証するなら、フレームワークが ARIA を手作業でマッピングし続ける必要は薄れる。ただし Flutter が明日これを採用する、という話ではない。Flutter の描画パイプラインは Skia が全ピクセルを支配する前提で組まれており、「特定のサブツリーだけブラウザに描かせる」ためには、レイアウト計算を Flutter 側と CSS 側のどちらが持つのかという根本的な設計判断が要る。それでも、ブラウザ側にこの受け皿ができることは、中期的には効いてくるはずだ。

実務で、今できること

現時点のステータスを正確に押さえておく。

  • Chrome: Chrome 148 でオリジントライアル開始。blink-dev の Intent to Experiment ではデスクトップ/Android/WebView の 148〜151 が対象とされ、その後延長されている(ICS MEDIA の解説では Chrome 155 まで延長と報告)
  • ローカル検証: Chrome Canary 149 以降で chrome://flags/#canvas-draw-element を有効化
  • 仕様: W3C WICG の提案段階(WICG/html-in-canvas)。実装詳細は変更されうると Chrome 側が明言している
  • Firefox: 未実装。フィンガープリンティングと互換性の懸念が継続中
  • Safari: 試験的実装が始まった段階

したがって、本番投入の判断をする時期ではない。今やる価値があるのは次のあたりだ。

Canvas ベースの UI を持つプロダクトを抱えているなら、「HTML-in-Canvas が来たら消せる自前実装」を棚卸ししておくとよい。自作のテキスト折り返しエンジン、Canvas 上の擬似フォーム、当たり判定テーブル、独自フォーカス管理 — これらは将来的に削除候補になる。設計上それらを差し替え可能な形に隔離しておけば、移行コストが変わる。

データビジュアライゼーションを書いているなら、注意点が一つある。テキストが読めるようになることと、グラフがアクセシブルになることは別物だ。軸ラベルがスクリーンリーダーに読まれても、それだけでは折れ線の傾向は伝わらない。従来どおり、意味的な要約やフォールバックのデータテーブルを用意する必要は残る。この API はアクセシビリティ対応の免罪符ではなく、土台の一部でしかない。

そして 3D/WebGPU 領域で UI を扱っているなら、ここは素直に期待していい。Three.js や PlayCanvas の抽象が固まった段階で、「3D 空間内に本物のフォームを置く」が現実的な選択肢になる。

まとめ

  • HTML-in-Canvas API は、layoutsubtree 属性・drawElementImage 系の描画メソッド・paint イベントの 3 プリミティブで構成される小さな API で、Canvas に実在の DOM 要素を描画する
  • 描画メソッドの戻り値の変換行列を実 DOM 側に適用することで、見た目と操作位置を同期させる。これによりテキスト選択・クリック判定・フォーカス・ページ内検索・翻訳が Canvas 越しに機能する
  • 2D だけでなく WebGL(texElementImage2D)と WebGPU(copyElementImageToTexture)にも入るため、3D 空間内 UI の実装手段として html2canvas や CSS3DRenderer の代替になりうる
  • IME 未確定文字列、スペルチェック波線、:visited の色などが意図的に除外されている。すべて Canvas 読み戻しによる情報漏洩を塞ぐための判断で、それでもフィンガープリンティングのリスクは残ると Chromium 側も認めている
  • Firefox は懸念を保持したまま未実装、Safari は試験実装段階。仕様は WICG の提案段階にあり、本番投入の判断はまだ早い

Canvas に描いたものが「絵」でしかなかった 20 年が終わろうとしている。実装の細部は動く可能性が高いが、「ピクセルの裏に意味を保持する」という方向性自体は、Flutter Web が独自に辿り着いた解と一致している。ブラウザ側にこの受け皿が標準として入るなら、キャンバス描画型フレームワークが背負ってきた負債の一部は、いずれ本来の持ち主に返せるようになる。


今日のその他のニュース

Qwen3.8-Flash-Next(125B total / 6B active)が公開 — Alibaba が MoE 構成の新モデルを ModelScope で公開した。アクティブパラメータが 6B に抑えられているため、総パラメータ 125B を載せられるメモリさえあればコンシューマ機でも実用速度が出る。同日に Apple が M6 / M5 Ultra と新 Mac Studio を発表しており(HN で 716pts / 582pts)、大容量ユニファイドメモリ機 × 大規模 MoE という組み合わせがローカル推論の現実解になりつつある。(Hacker News)

/model を一度でも実行すると ANTHROPIC_DEFAULT_MODEL が効かなくなる — Claude Code で /model を実行すると設定ファイルにモデルが永続化され、以降は環境変数が無視される挙動。launchd や cron から Claude Code を自動実行している構成では、手元で /model を触った瞬間に自動実行側のモデルまで固定される。意図しないモデルで走っていないか設定ファイル側を確認しておきたい。(Qiita)

Firebase App Check が reCAPTCHA Enterprise に対応(Preview) — Apple / Android / Flutter アプリで reCAPTCHA Enterprise をアテステーションプロバイダとして選べるようになった。従来の DeviceCheck / Play Integrity に加えての選択肢になる。あわせて Firebase AI Logic が gemini-3.7-flash に対応し、Firebase CLI v15.26.0 が二段階の非対話ログインを追加している。CI での認証まわりに影響する変更だ。(Releasebot)

ソース