HTML-in-Canvas : Rendez de Vrais Éléments DOM dans un Canvas avec la Nouvelle API drawElement

La plateforme web vient de gagner un superpouvoir. La proposition WICG HTML-in-Canvas — désormais derrière un flag dans Chromium — vous permet de rendre de vrais éléments HTML interactifs directement dans un <canvas>. Formulaires, texte, tableaux de bord, widgets UI complets — dessinés dans des contextes 2D ou WebGL tout en conservant l'accessibilité complète du DOM.
Fini les hacks html2canvas. Fini les contournements foreignObject SVG. Du rendu HTML natif, de première classe, à l'intérieur du canvas.
Dans ce tutoriel, vous allez construire 4 démos qui montrent ce que cette API rend possible — et comprendre l'architecture qui la sous-tend.
Prérequis
- Node.js 18+
- Next.js 14+ (App Router)
- Chrome Canary avec
chrome://flags/#canvas-draw-elementactivé - Connaissances de base de l'API Canvas
Le Problème Résolu
Jusqu'à présent, rendre du contenu HTML dans un canvas nécessitait des solutions de contournement pénibles :
| Approche | Limitation |
|---|---|
html2canvas | Re-rend le DOM en bitmap — pas d'interactivité, rendu imprécis |
foreignObject en SVG | Canvas contaminé, problèmes CORS, pas de support WebGL |
| Dessin Canvas manuel | Vous perdez tout le CSS, l'accessibilité, l'i18n, la mise en page du texte |
| Texte WebGL | Nécessite des atlas de polices, pas de mises en page complexes, surcharge massive |
L'API HTML-in-Canvas résout tout cela en laissant le moteur de rendu du navigateur dessiner les éléments DOM dans un contexte canvas.
L'API : Trois Primitives
1. L'attribut layoutsubtree
<canvas layoutsubtree width="800" height="600">
<!-- Ces enfants sont de VRAIS éléments DOM -->
<div class="card">
<h2>Je suis un vrai titre</h2>
<button>Je suis un vrai bouton</button>
</div>
</canvas>L'attribut layoutsubtree dit au navigateur : « Dispose mes enfants comme des éléments DOM normaux, mais rends-les aussi disponibles pour le dessin dans le canvas. » Les enfants obtiennent :
- Un contexte d'empilement
- Un comportement de bloc conteneur
- Un confinement de peinture
- Un hit testing complet et l'accessibilité
2. drawElement() pour le Canvas 2D
const ctx = canvas.getContext('2d');
const transform = ctx.drawElement(childElement, x, y, { width, height });
childElement.style.transform = transform;Cela dessine l'élément enfant — incluant tous ses styles CSS, pseudo-éléments, ombres et mise en page — dans le canvas à la position (x, y). La transformation retournée aligne la position DOM avec la position dessinée pour que les événements fonctionnent correctement.
3. texElement2D() pour WebGL
const gl = canvas.getContext('webgl2');
gl.texElement2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, childElement);Cela télécharge le rendu de l'élément comme texture WebGL — permettant des transformations 3D, de l'éclairage, du post-traitement et des effets de shader sur du vrai contenu HTML.
Configuration
npx create-next-app@latest html-in-canvas-demo --typescript --tailwind --app
cd html-in-canvas-demoDémo 1 : Dessiner du HTML dans un Canvas (Basique)
Le cas d'utilisation le plus simple — prendre un élément HTML et le dessiner dans un canvas 2D.
// app/basic/page.tsx
'use client';
import { useEffect, useRef } from 'react';
export default function BasicDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const card = cardRef.current;
if (!canvas || !card) return;
const ctx = canvas.getContext('2d')!;
canvas.width = 800;
canvas.height = 500;
// L'événement paint se déclenche quand les enfants changent
canvas.addEventListener('paint', () => {
ctx.clearRect(0, 0, 800, 500);
// Dessiner le fond sombre
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, 800, 500);
// Dessiner l'élément HTML card à la position (200, 100)
// drawElement rend le sous-arbre DOM stylé COMPLET
const transform = ctx.drawElement(card, 200, 100, {
width: 400,
height: 300,
});
// Appliquer la transformation pour que les événements souris touchent le bon endroit
card.style.transform = transform;
});
}, []);
return (
<canvas ref={canvasRef} layoutsubtree width={800} height={500}>
{/* Ceci est un VRAI élément DOM à l'intérieur du canvas */}
<div
ref={cardRef}
className="rounded-xl bg-slate-800 p-8 text-white shadow-2xl"
>
<h2 className="text-2xl font-bold mb-4">Bonjour depuis le DOM</h2>
<p className="text-slate-300 mb-6">
Cette carte est un vrai élément HTML — avec un style CSS complet,
l'accessibilité et la gestion des événements — dessinée dans un canvas.
</p>
<button
className="rounded-lg bg-sky-500 px-6 py-2 font-semibold
hover:bg-sky-400 transition-colors"
onClick={() => alert('Bouton cliqué ! Les événements fonctionnent.')}
>
Cliquez-moi
</button>
</div>
</canvas>
);
}Ce qui se passe :
- L'attribut
layoutsubtreedit à Chrome de disposer le<div>comme un élément DOM normal ctx.drawElement()le rend dans le canvas à(200, 100)avec les dimensions spécifiées- La
transformretournée est appliquée à l'élément pour que les événements de clic correspondent à la bonne position - L'événement
paintse déclenche chaque fois que l'élément enfant change — pas de polling manuel nécessaire
Démo 2 : Formulaire Interactif dans le Canvas
De vrais formulaires — avec états de focus, navigation au clavier et validation — à l'intérieur d'un canvas.
// app/form/page.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
export default function FormDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
const canvas = canvasRef.current;
const form = formRef.current;
if (!canvas || !form) return;
const ctx = canvas.getContext('2d')!;
canvas.width = 900;
canvas.height = 600;
canvas.addEventListener('paint', () => {
ctx.clearRect(0, 0, 900, 600);
// Dégradé de fond
const grad = ctx.createLinearGradient(0, 0, 900, 600);
grad.addColorStop(0, '#0f172a');
grad.addColorStop(1, '#1e1b4b');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 900, 600);
// Cercles décoratifs
ctx.beginPath();
ctx.arc(700, 100, 200, 0, Math.PI * 2);
ctx.fillStyle = '#38bdf820';
ctx.fill();
ctx.beginPath();
ctx.arc(200, 500, 150, 0, Math.PI * 2);
ctx.fillStyle = '#a78bfa15';
ctx.fill();
// Dessiner le formulaire au centre
const transform = ctx.drawElement(form, 250, 80, {
width: 400,
height: 440,
});
form.style.transform = transform;
});
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
setTimeout(() => setSubmitted(false), 2000);
};
return (
<canvas ref={canvasRef} layoutsubtree width={900} height={600}>
<form
ref={formRef}
onSubmit={handleSubmit}
className="rounded-2xl bg-slate-800/90 backdrop-blur p-8 shadow-2xl
border border-slate-700"
>
<h2 className="text-xl font-bold text-white mb-6">Contactez-nous</h2>
<label className="block mb-4">
<span className="text-sm text-slate-400">Nom</span>
<input
type="text"
required
className="mt-1 w-full rounded-lg bg-slate-900 border border-slate-600
px-4 py-2.5 text-white focus:border-sky-400
focus:ring-2 focus:ring-sky-400/30 outline-none"
placeholder="Votre nom"
/>
</label>
<label className="block mb-4">
<span className="text-sm text-slate-400">Email</span>
<input
type="email"
required
className="mt-1 w-full rounded-lg bg-slate-900 border border-slate-600
px-4 py-2.5 text-white focus:border-sky-400
focus:ring-2 focus:ring-sky-400/30 outline-none"
placeholder="vous@exemple.com"
/>
</label>
<label className="block mb-6">
<span className="text-sm text-slate-400">Message</span>
<textarea
rows={4}
className="mt-1 w-full rounded-lg bg-slate-900 border border-slate-600
px-4 py-2.5 text-white focus:border-sky-400
focus:ring-2 focus:ring-sky-400/30 outline-none resize-none"
placeholder="Comment pouvons-nous vous aider ?"
/>
</label>
<button
type="submit"
className={`w-full rounded-lg py-3 font-semibold transition-all ${
submitted
? 'bg-green-500 text-white'
: 'bg-sky-500 text-white hover:bg-sky-400'
}`}
>
{submitted ? 'Envoyé !' : 'Envoyer le message'}
</button>
</form>
</canvas>
);
}Pourquoi c'est important : Le formulaire est un vrai élément DOM. Les lecteurs d'écran le voient. La touche Tab fonctionne. Le remplissage automatique du navigateur fonctionne. Les gestionnaires de mots de passe fonctionnent. Le tout rendu dans un canvas avec des fonds décoratifs personnalisés qui seraient impossibles avec du DOM pur.
Démo 3 : Widgets de Tableau de Bord dans le Canvas
Composez plusieurs widgets HTML dans un seul canvas — avec un compositing accéléré par GPU.
// app/dashboard/page.tsx
'use client';
import { useEffect, useRef } from 'react';
function StatCard({ title, value, change, color }: {
title: string; value: string; change: string; color: string;
}) {
return (
<div className="rounded-xl bg-slate-800 p-5 border border-slate-700">
<p className="text-sm text-slate-400">{title}</p>
<p className="text-2xl font-bold text-white mt-1">{value}</p>
<p className={`text-sm font-medium mt-2`} style={{ color }}>
{change}
</p>
</div>
);
}
export default function DashboardDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const card1Ref = useRef<HTMLDivElement>(null);
const card2Ref = useRef<HTMLDivElement>(null);
const card3Ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d')!;
canvas.width = 900;
canvas.height = 500;
const cards = [card1Ref, card2Ref, card3Ref];
canvas.addEventListener('paint', () => {
ctx.clearRect(0, 0, 900, 500);
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, 900, 500);
// Titre dessiné avec l'API Canvas
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 24px Inter, system-ui, sans-serif';
ctx.fillText('Tableau de Bord Analytique', 30, 40);
// Dessiner chaque carte stat à différentes positions
const positions = [
{ x: 30, y: 70 },
{ x: 310, y: 70 },
{ x: 590, y: 70 },
];
cards.forEach((ref, i) => {
if (ref.current) {
const t = ctx.drawElement(ref.current, positions[i].x, positions[i].y, {
width: 250,
height: 120,
});
ref.current.style.transform = t;
}
});
// Dessiner un graphique avec l'API Canvas sous les cartes HTML
drawChart(ctx, 30, 220, 840, 250);
});
}, []);
return (
<canvas ref={canvasRef} layoutsubtree width={900} height={500}>
<div ref={card1Ref}>
<StatCard title="Revenus" value="$12,847" change="+12.5%" color="#4ade80" />
</div>
<div ref={card2Ref}>
<StatCard title="Utilisateurs" value="3,429" change="+8.2%" color="#38bdf8" />
</div>
<div ref={card3Ref}>
<StatCard title="Commandes" value="842" change="+23.1%" color="#a78bfa" />
</div>
</canvas>
);
}
function drawChart(
ctx: CanvasRenderingContext2D,
x: number, y: number, w: number, h: number
) {
// Fond du graphique
ctx.fillStyle = '#1e293b';
ctx.beginPath();
ctx.roundRect(x, y, w, h, 12);
ctx.fill();
// Ligne du graphique
const points = [40, 65, 45, 80, 60, 90, 75, 95, 70, 100, 85, 110];
ctx.beginPath();
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 2.5;
points.forEach((p, i) => {
const px = x + 30 + (i / (points.length - 1)) * (w - 60);
const py = y + h - 30 - (p / 120) * (h - 60);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
});
ctx.stroke();
// Remplissage en dégradé
const lastPx = x + 30 + ((points.length - 1) / (points.length - 1)) * (w - 60);
ctx.lineTo(lastPx, y + h - 30);
ctx.lineTo(x + 30, y + h - 30);
ctx.closePath();
const grad = ctx.createLinearGradient(0, y, 0, y + h);
grad.addColorStop(0, '#38bdf830');
grad.addColorStop(1, '#38bdf805');
ctx.fillStyle = grad;
ctx.fill();
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 14px Inter, system-ui';
ctx.fillText('Revenus au Fil du Temps', x + 16, y + 28);
}La puissance ici : Les cartes de statistiques HTML vous donnent un style CSS complet, des états de survol et l'accessibilité — tandis que le graphique en dessous est dessiné avec l'API Canvas pour un rendu au pixel près. L'événement paint garde tout synchronisé. Une seule surface de rendu, le meilleur des deux mondes.
Démo 4 : HTML comme Texture WebGL (Carte 3D)
La primitive la plus excitante — téléchargez un élément HTML comme texture WebGL et appliquez des transformations 3D.
// app/3d-card/page.tsx
'use client';
import { useEffect, useRef } from 'react';
export default function ThreeDCardDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const profileRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const profile = profileRef.current;
if (!canvas || !profile) return;
const gl = canvas.getContext('webgl2')!;
canvas.width = 800;
canvas.height = 600;
// Créer une texture à partir de l'élément HTML
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Télécharger l'élément HTML comme texture
// texElement2D rend la sortie stylée complète de l'élément
gl.texElement2D(
gl.TEXTURE_2D, // cible
0, // niveau
gl.RGBA, // format interne
gl.RGBA, // format
gl.UNSIGNED_BYTE, // type
profile // l'élément HTML
);
// Paramètres de texture
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Maintenant utilisez cette texture dans votre scène WebGL
// Appliquez des transformations de perspective, éclairage, réflexions...
// Le contenu HTML devient un objet 3D de première classe
renderScene(gl, texture);
}, []);
return (
<canvas ref={canvasRef} layoutsubtree width={800} height={600}>
<div
ref={profileRef}
className="w-72 rounded-2xl bg-gradient-to-b from-slate-800 to-slate-900
p-8 text-center border border-slate-700 shadow-2xl"
>
<div className="w-20 h-20 mx-auto rounded-full bg-gradient-to-br
from-sky-400 to-violet-500 flex items-center
justify-center text-3xl font-bold text-white mb-4">
N
</div>
<h3 className="text-xl font-bold text-white">Noqta Agent</h3>
<p className="text-sky-400 text-sm mt-1">Agence de Développement IA</p>
<div className="flex justify-around mt-6 text-center">
<div>
<p className="text-lg font-bold text-white">127</p>
<p className="text-xs text-slate-400">Projets</p>
</div>
<div>
<p className="text-lg font-bold text-white">48</p>
<p className="text-xs text-slate-400">Clients</p>
</div>
<div>
<p className="text-lg font-bold text-white">2.4k</p>
<p className="text-xs text-slate-400">Étoiles</p>
</div>
</div>
<button className="mt-6 w-full rounded-lg bg-gradient-to-r
from-sky-500 to-violet-500 py-2.5 font-semibold
text-white hover:opacity-90 transition">
Voir le Profil
</button>
</div>
</canvas>
);
}
function renderScene(gl: WebGL2RenderingContext, texture: WebGLTexture | null) {
// Vertex shader pour une carte rotative
const vsSource = `#version 300 es
in vec4 aPosition;
in vec2 aTexCoord;
uniform mat4 uProjection;
uniform mat4 uModelView;
out vec2 vTexCoord;
void main() {
gl_Position = uProjection * uModelView * aPosition;
vTexCoord = aTexCoord;
}
`;
// Fragment shader — applique la texture HTML
const fsSource = `#version 300 es
precision highp float;
in vec2 vTexCoord;
uniform sampler2D uTexture;
out vec4 fragColor;
void main() {
fragColor = texture(uTexture, vTexCoord);
}
`;
// ... configuration WebGL standard : compiler les shaders, créer le programme,
// configurer la géométrie de la carte, animer la rotation avec requestAnimationFrame
// L'idée clé : la texture EST la sortie rendue de l'élément HTML
}Ce que texElement2D permet :
- Des cartes produit 3D avec du vrai contenu HTML
- Des effets de texte en perspective avec une typographie CSS complète
- Des transitions basées sur les shaders entre vues HTML
- Des interfaces de réalité mixte avec du HTML dans des scènes WebXR
Comment Fonctionne l'Événement paint
L'événement paint est le mécanisme de synchronisation. Il se déclenche quand un enfant du canvas change — re-rendus, animations, changements de style.
canvas.addEventListener('paint', () => {
// Un instantané du rendu de tous les enfants est capturé
// AVANT que cet événement ne se déclenche. Quand vous appelez
// drawElement() ici, il utilise cet instantané — pas de passe
// de rendu supplémentaire.
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawElement(myElement, 0, 0);
});Cela signifie :
- Pas de double rendu — le navigateur capture les pixels des enfants dans la même passe de compositing
- Pas de
requestAnimationFramemanuel — l'événement paint se déclenche au bon moment - Regroupement automatique — plusieurs changements d'enfants déclenchent un seul événement paint
Support Navigateur (Avril 2026)
| Navigateur | Statut |
|---|---|
| Chrome Canary | Derrière le flag #canvas-draw-element |
| Chrome Stable | Attendu Q3 2026 |
| Firefox | En cours de considération |
| Safari | Aucun signal pour le moment |
Pour une utilisation en production aujourd'hui, vous aurez besoin d'une stratégie d'amélioration progressive :
function CanvasWithFallback({ children }: { children: React.ReactNode }) {
const supportsDrawElement = typeof HTMLCanvasElement !== 'undefined'
&& 'drawElement' in CanvasRenderingContext2D.prototype;
if (!supportsDrawElement) {
// Rendre comme DOM normal
return <div className="canvas-fallback">{children}</div>;
}
return (
<canvas layoutsubtree width={800} height={600}>
{children}
</canvas>
);
}Ce Que Vous Avez Construit
| Démo | API Utilisée | Capacité |
|---|---|---|
| HTML dans Canvas | drawElement() | Rendre n'importe quel HTML stylé dans un canvas 2D |
| Formulaire Interactif | drawElement() + événements | Formulaire complet avec focus, validation, a11y |
| Tableau de Bord | drawElement() + API Canvas | Mixer widgets HTML avec graphiques canvas |
| Carte 3D Profil | texElement2D() | Contenu HTML comme texture WebGL avec transformations 3D |
Pourquoi HTML-in-Canvas Change Tout
Avant cette API, canvas et DOM étaient deux mondes séparés. Vous pouviez soit :
- Utiliser le DOM et renoncer aux performances/effets du canvas
- Utiliser le canvas et renoncer à l'accessibilité, l'i18n, la mise en page du texte, les événements
- Utiliser
html2canvaset obtenir une capture d'écran cassée
HTML-in-Canvas comble le fossé. Le moteur de rendu du navigateur dessine les éléments DOM dans le canvas — avec une fidélité totale, une interactivité complète et un compositing accéléré par GPU.
Cela ouvre la voie à une nouvelle classe d'applications web :
- Outils créatifs (type Figma/Canva) avec édition de texte réelle dans le canvas
- Interfaces de jeux avec menus HTML accessibles superposés sur des scènes WebGL
- Visualisation de données avec infobulles et étiquettes HTML dans les graphiques canvas
- Outils de présentation avec transitions 3D entre diapositives HTML
Vous construisez une application intensive en canvas ? Nos agents livrent du Next.js en production avec Canvas avancé, WebGL et creative coding — 45$/h, avec supervision humaine. Réservez un appel gratuit
Et Ensuite ?
- Éditeur type Figma avec édition de texte réelle dans les objets canvas
- Éditeur photo avec panneaux de contrôle HTML rendus dans l'espace de travail canvas
- Portfolio 3D avec cartes de profil HTML flottant dans une scène WebGL
- Tableau de bord de données interactif mélangeant graphiques D3 et widgets HTML
L'API HTML-in-Canvas est le pont manquant entre DOM et canvas. Les jours où il fallait choisir l'un ou l'autre sont révolus.
Besoin d'aide pour construire un produit basé sur le canvas ? Des outils créatifs aux tableaux de bord de données, nos agents IA gèrent le code Canvas/WebGL complexe pendant que vous vous concentrez sur le produit. Parlez à un agent
Lectures Complémentaires
- Construire des Effets de Texte Époustouflants avec Pretext et Next.js
- Proposition WICG HTML-in-Canvas
- Chromium Status : HTML-in-Canvas
Canvas nous a donné les pixels. Le DOM nous a donné la structure. HTML-in-Canvas nous donne les deux — en même temps, dans le même élément.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire une Application d'IA Conversationnelle avec Next.js
Apprenez a construire une application web qui permet des conversations vocales en temps reel avec des agents IA en utilisant Next.js et ElevenLabs.

Construire une Application Multi-Tenant avec Next.js
Apprenez a construire une application multi-tenant full-stack en utilisant Next.js, Vercel et d'autres technologies modernes.

Creer un Podcast a partir d'un PDF avec Vercel AI SDK et LangChain
Apprenez a creer un podcast a partir d'un PDF en utilisant Vercel AI SDK, PDFLoader de LangChain, ElevenLabs et Next.js.