Canvas内にHTMLを直接描画するHTML-in-canvasの仕組みと使い方

Webサイト(ホームページ)のトップページやLPのファーストビューなどでは、Canvasを使って背景にパーティクルや幾何学模様、波紋などの動的なビジュアルを表現して、その前面にキャッチコピーやカードUIを配置するレイアウトが定番の手法として広く使われています。

ただ、グラフィックスとUIの共存には、構造的な課題などがありました。 Canvas単体では複数行のテキストや複雑な装飾を扱うのが難しいため、多くの現場ではCanvasの上にCSSの position: absolute を使ってHTMLを重ねるといった仕様でデザインを構築してきました。
一見綺麗に動いているように見えても、CanvasとDOMが別々のレイヤーとして分断されているため、背景の演出とUIの動きを完全に同期させたり、Canvas側のエフェクトや変形をUI側にもそのまま波及させたりすることは難しいです。

そこで新しく仕様策定・実装が進められているのが「HTML-in-canvas」です。
HTML-in-canvasは、HTMLをCanvasの上に物理的に重ねるのではなく、HTML要素そのものをCanvasの描画処理へ直接流し込んで描画する仕組みです。

ここでは、Web標準のCSSスタイルや文字の選択性をそのまま維持しながら、Canvasの描画処理の中にUIを組み込める仕組みについて、具体的な挙動やコードを交えてご紹介します。

これまでの実装方法との違い


Web制作において、Canvasの背景演出の上にUIを配置する手法としては、CSSの position: absolute で上に重ねる方法が使われてきました。ただ、この手法はあくまでブラウザの表示ツリー上で2つの独立したレイヤーを前後に並べているにすぎません。

HTML-in-canvasは、HTML要素をCanvasの描画処理に直接渡して、ひとつの描画処理の流れの中でまとめてレンダリングします。 外見上はどちらも動く背景の上にカードが乗っているように見えますが、内部のレンダリング構造は大きく異なります。

CSSで重ねる場合
CanvasとDOMは独立したレイヤーとして扱われます。
ブラウザは背景のCanvasと手前のHTMLを別々に描画した上で画面に合成するため、Canvas側で背景にフィルターをかけたり、色合いやブレンドモードを調整したりしても、前面に浮いているHTMLには影響を与えられません。また、Canvas側でオブジェクトが手前に回り込むような描画順序の制御も不可能です。
HTML-in-canvasの場合
HTML要素をCanvasの描画コンテキストへ直接渡してレンダリングします。
グラフィックスの描画ステップの中にHTML要素の描画処理をそのまま組み込めるため、描画順序の制御や各種エフェクトの同期が容易になります。さらに、描画位置や変形をCanvas側でコントロールしていても、テキストの選択やクリック判定などのDOM操作はブラウザが追従して処理します。

主な特徴

HTML要素をCanvasの描画処理に直接組み込むことで、これまでの実装方法では難しかった表現や操作が可能になります。

CSSのスタイルがそのまま反映される
角丸やドロップシャドウ、半透明の背景色など、通常のWeb制作で使うCSSスタイルをそのままCanvas上に描画できます。
Canvas特有の描画命令を一から組み立てる必要がなく、既存のCSS設計をそのまま活かせます。
テキスト選択やクリックが可能
従来の fillText() で描いたテキストは単なるピクセル情報となり、マウス操作を受け付けません。
HTML-in-canvasで描画された要素は通常のDOM要素として機能するため、画面上の文字をドラッグしてコピーしたり、配置したリンクやボタンをクリックしたりといった操作がそのまま機能します。
Canvas側の描画処理と同期できる
Canvasの描画ループ(アニメーションフレーム)に直接組み込めるため、「特定のオブジェクトを描いた後にカードを描き、その上にさらにエフェクトを被せる」といった前後関係の制御が可能です。
Canvas側の描画タイミングに合わせてUIの表示を完全にコントロールできます。

HTML-in-canvasは、これまで別々のレイヤーとして処理されていたCanvasとHTMLの描画をひとつにまとめてくれます。
HTMLの持つ操作性や装飾のしやすさを維持したまま、Canvasの描画処理の中にUIを完全に組み込めるところが、従来の実装手法と大きく異なります。

基本的な実装方法


ここからはサンプルコードをもとに、HTML-in-canvasの実装手順を見ていきます。
画面全体にCanvasの背景アニメーションを流して、その中央にHTMLで組んだカードUIを描画します。

まずはHTML。
<canvas> に content="drawable" を指定して、内部に配置するHTML要素にdrawable属性を付けます。

HTML

<!-- content="drawable"でキャンバス描画を有効化 -->
<canvas id="myCanvas" content="drawable">
  <!-- キャンバス内に描画するDOM要素(drawable属性を付与) -->
  <div id="card" drawable>
    <h2>HTML-in-Canvas</h2>
    <p>このカードは<strong>Canvas内部</strong>に描画されています。背景アニメーション中も、文字の選択やコピー、フォーカス枠の表示が動作します。</p>
  </div>
</canvas>


<canvas content="drawable"> の内側に置いた要素は、通常の画面上には直接表示されず、Canvasへの描画ソースとして扱われます。
また、実際にCanvas側へ描画したい要素にはdrawable属性を付与しておく必要があり、これによってJavaScriptから描画対象として指定できるようになります。

続いてCSSの記述です。
カードのサイズや装飾をCSSで指定します。

CSS

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #0f172a;
  font-family: system-ui, -apple-system, sans-serif;
}

#myCanvas {
  display: block;
  width: 100vw;
  height: 100vh;
}

#card {
  width: 320px;
  padding: 1.5rem;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(8px);
  border-radius: 1rem;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
  cursor: text;
  h2 {
    font-size: 1.25rem;
    color: #0f172a;
    margin-bottom: 0.5rem;
    margin-inline: auto;
    width: fit-content;
  }
  p {
    font-size: 0.9rem;
    line-height: 1.6;
    color: #334155;
  }
}


border-radius や box-shadow を含め、CSSで設定した通りの見た目でCanvas上に転写されます。
cursor: text を指定しておくことで、Canvas上のカードにマウスが乗った際、通常のテキストカーソルに切り替わります。

最後に、描画処理とアニメーションを制御するJavaScriptです。
背景のグラフィックを描画するアニメーションループの中で、ctx.drawElementImage() を呼び出してカードUIを画面中央に描画します。

JavaScript

const canvas = document.getElementById('myCanvas');
const card = document.getElementById('card');
const ctx = canvas.getContext('2d');

// 画面解像度に合わせてCanvasサイズを調整
function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();

// 背景パーティクルの初期設定(25個)
const particles = Array.from({ length: 25 }, () => ({
  x: Math.random() * window.innerWidth,
  y: Math.random() * window.innerHeight,
  radius: Math.random() * 60 + 20,
  vx: (Math.random() - 0.5) * 1.5,
  vy: (Math.random() - 0.5) * 1.5,
  hue: Math.random() * 60 + 200 // 青〜紫系
}));

function animate() {
  // 背景のクリアとグラデーション描画
  const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
  gradient.addColorStop(0, '#0f172a');
  gradient.addColorStop(1, '#1e293b');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // パーティクルの移動と描画
  particles.forEach(p => {
    p.x += p.vx;
    p.y += p.vy;

    // 画面外に出たら反対側から戻す
    if (p.x < -p.radius) p.x = canvas.width + p.radius;
    if (p.x > canvas.width + p.radius) p.x = -p.radius;
    if (p.y < -p.radius) p.y = canvas.height + p.radius;
    if (p.y > canvas.height + p.radius) p.y = -p.radius;

    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
    ctx.fillStyle = `hsla(${p.hue}, 80%, 60%, 0.15)`;
    ctx.fill();
  });

  // カードを配置する中央の座標を計算
  const cardWidth = card.offsetWidth || 320;
  const cardHeight = card.offsetHeight || 180;
  const posX = (canvas.width - cardWidth) / 2;
  const posY = (canvas.height - cardHeight) / 2;

  // HTMLカードをCanvasに直接描画
  ctx.drawElementImage(card, posX, posY);

  requestAnimationFrame(animate);
}

window.addEventListener('load', animate);

drawElementImageの仕組み

従来の ctx.drawImage() は、画像や動画、別のCanvasなどすでに画像化されたデータしか引数に取れず、通常のHTML要素を渡すことはできませんでした。そのため、HTMLをCanvasに描くには外部ライブラリを使ってDOMを画像に変換するなどの重い処理が必要で、毎フレーム動くアニメーションでの利用には不向きでした。

ctx.drawElementImage(card, posX, posY) では、引数に生のDOM要素をそのまま渡すことができます。ブラウザが内部でCSSのスタイルを適用した状態のまま直接レンダリングするため、外部ライブラリを介さず高速に動作します。

さらに重要なのが、描画位置と当たり判定の自動同期です。ピクセルを描き出すだけでなく、ブラウザに対してこのHTML要素がCanvas上のどの座標にあるかを登録するため、Canvas上であってもユーザーが通常通りテキストをドラッグして選択したり、リンクをクリックしたりできるようになります。

Canvasの背景アニメーションの上にHTML-in-canvasでカードUIを描画した実行結果
HTML-in-canvasによるCanvas背景とカードUIの描画


画面中央にカードを配置するために card.offsetWidth と card.offsetHeight を参照しています。Canvasの内側に配置した要素であっても、ブラウザ内部でCSSのレイアウト計算は正しく行われているため、JavaScriptから実際の表示幅や高さを取得して正確な座標計算に利用できます。

毎フレーム実行される requestAnimationFrame のループ処理では、背景を一度クリアしてパーティクルを描き直した直後に drawElementImage() を呼び出しています。Canvas自体は常に上書き再描画されていますが、カード内のテキストをドラッグ選択している最中でも選択状態が解除されたり表示が乱れたりすることなく、通常のHTMLと同じ感覚で操作できます。

最後に


HTML-in-canvasは、これまでCSSで重ねていたWeb UIとグラフィックスを連携させ、Canvasの描画フローの中にそのままDOMを持ち込める仕組みです。文字の選択性や装飾のしやすさといったHTML/CSSの強みをそのまま活かせるため、動的な背景演出を伴うファーストビューやインタラクティブな表現の実装手順を簡略化できます。

ブラウザのサポート状況への配慮や、過度なCSS装飾による描画負荷といった留意点はありますが、ピクセル描画とDOM操作をひとつのコンテキストで扱えるメリットは大きいです。リッチな表現と使いやすさを両立させる新しい選択肢として、今後の普及と活用が期待されます。