



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp








生徒CSSだけでは表現できない複雑な背景パターンを実装したいんですが、画像を使うと重いし……カスタム描画ができるAPIがあるんですか?



CSS Painting APIがまさにそれじゃ!CSSのHoudiniプロジェクトの一部で、PaintWorkletというJavaScriptのクラスを使って任意の描画処理をCSS内で呼び出せるぞ。画像ファイル不要で、CSS変数でパラメーターも動的に変えられるから非常に強力じゃ!さらに@propertyと組み合わせてアニメーションも実装できる!
結論:CSS Painting API(CSS Houdini)はPaintWorkletをJavaScriptで定義し、background-imageにpaint()で呼び出すことで、画像ファイル不要のカスタム背景パターンを実装できます。CSS変数でパラメーターをCSSから動的に制御でき、@propertyと組み合わせることでCSSアニメーションも可能です。
本記事では、CSS Painting APIの概要・CSS Houdiniのアーキテクチャ・PaintWorkletの登録方法・inputProperties/inputArguments・カスタムプロパティとの連携・ドット/波線/チェッカー/メッシュ/ノイズ背景の実装・CSS @propertyとのアニメーション連携・Viteでのバンドル方法・SVGとの使い分け・css-paint-polyfillによる互換対応・ブラウザサポート状況を完全解説します。
CSS Houdiniは、開発者がブラウザのCSSエンジンに直接アクセスするための低レベルAPIセットです。従来はブラウザ側だけが実装できたCSSの機能を、JavaScriptを使って開発者が独自に実装できるようになります。
>【CSS Houdiniの主なAPI】
1. CSS Painting API(本記事のメイン)
→ background-image, border-image等にカスタム描画を適用
→ PaintWorkletで任意のCanvas描画を定義
→ ブラウザサポート:Chrome/Edge ✅ / Firefox ⚠️ / Safari ❌
2. CSS Properties and Values API(@property)
→ CSSカスタムプロパティに型・初期値・継承を定義
→ アニメーション対応(transition/animation が効くようになる)
→ ブラウザサポート:Chrome/Edge/Safari ✅ / Firefox ✅(2023+)
3. CSS Layout API
→ display プロパティのカスタムレイアウトを実装
→ Masonry レイアウトなどを自前で実装可能
→ ブラウザサポート:Chrome ⚠️(実験的)
4. CSS Animation Worklet API
→ アニメーションをメインスレッドから切り離して滑らか化
→ ブラウザサポート:Chrome ⚠️(実験的)
5. CSS Parser API
→ CSSパース結果へのプログラムアクセス
→ ブラウザサポート:検討中
【Workletとは】
→ Service WorkerやWeb Workerと同様の分離された実行コンテキスト
→ メインスレッドとは別のスレッドで動作
→ DOM へのアクセスはできない(セキュリティのため)
→ グローバル変数は共有しない

>【CSS Painting APIの仕組み】
通常のCSS(画像を使う場合):
background-image: url('pattern.png')
→ 外部画像ファイルが必要。解像度が固定。
→ 小さいサイズではpixelが荒くなる。
→ CSSで動的に変更不可。
CSS Painting API(カスタム描画):
background-image: paint(myPattern)
→ JavaScriptのPaintWorkletで任意の描画を実行
→ ファイル不要・サイズに応じた動的描画(ベクター的)
→ CSS変数でパラメーターをCSSから渡せる
→ CSS transition/animationの値変化に合わせて再描画
【Canvas APIとの違い】
Canvas API(通常):
→ <canvas> 要素に描画(DOM要素)
→ メインスレッドで実行
→ CSS で位置・サイズを制御
CSS Painting API:
→ CSSプロパティの値として描画(DOM要素ではない)
→ ワークレットスレッドで実行(パフォーマンス良好)
→ 要素のサイズ変更に自動追従(リサイズ対応)
【ユースケース一覧】
✅ カスタム背景パターン(ドット・波・メッシュ・ハッチング・ノイズ)
✅ カスタムボーダー・アウトライン(波線・破線・ジグザグ)
✅ 動的なグラデーション(データに基づいた動的な配色)
✅ データビジュアライゼーション(簡易的なプログレスバー・ミニチャート)
✅ カスタムハイライト・テキスト装飾>// paint-worklet.js
// ※ このファイルはグローバルスコープが通常のJSと異なる
// - document, window はアクセス不可
// - registerPaint() でWorkletを登録
class CheckerboardPainter {
// CSSから渡せるカスタムプロパティを定義(inputPropertiesに指定)
static get inputProperties() {
return ['--checkerboard-size', '--checkerboard-color']
}
// paint() が実際の描画処理
// ctx: PaintRenderingContext2D(Canvas2D互換)
// geometry: 描画領域のサイズ(geometry.width, geometry.height)
// properties: inputPropertiesで指定したCSS変数の値
paint(ctx, geometry, properties) {
// CSS変数を取得
const rawSize = properties.get('--checkerboard-size')
const size = rawSize ? parseInt(rawSize.toString()) : 20
const rawColor = properties.get('--checkerboard-color')
const color = rawColor ? rawColor.toString().trim() : '#cccccc'
const cols = Math.ceil(geometry.width / size) + 1
const rows = Math.ceil(geometry.height / size) + 1
// 市松模様:行列の合計が偶数のセルを塗る
for (let row = 0; row <= rows; row++) {
for (let col = 0; col <= cols; col++) {
if ((row + col) % 2 === 0) {
ctx.fillStyle = color
ctx.fillRect(col * size, row * size, size, size)
}
}
}
}
}
// ワークレットに名前を付けて登録(paint()から呼び出すときの名前)
registerPaint('checkerboard', CheckerboardPainter)><!-- index.html -->
<script>
// CSS Painting APIのサポート確認
if ('paintWorklet' in CSS) {
// PaintWorkletモジュールを非同期で読み込む
CSS.paintWorklet.addModule('./paint-worklet.js')
.then(() => console.log('PaintWorklet loaded'))
.catch(err => console.error('Failed to load worklet:', err))
} else {
console.warn('CSS Painting API is not supported in this browser')
// Polyfillを読み込む(後述)
}
</script>>/* style.css */
.checkerboard-bg {
/* CSS変数でパラメーターを渡す */
--checkerboard-size: 30;
--checkerboard-color: #3B82F620; /* 半透明の青 */
/* paint() でWorkletを呼び出す(registerPaintの名前と一致) */
background-image: paint(checkerboard);
background-color: #ffffff; /* フォールバック(未対応ブラウザ用) */
width: 100%;
height: 300px;
border-radius: 8px;
}
/* ホバーで色を変える(CSS変数の変更で自動的にpaintが再実行される!) */
.checkerboard-bg:hover {
--checkerboard-color: #EF444420;
--checkerboard-size: 15;
transition: none; /* CSS変数はtransitionできない(後述の@propertyで解決)*/
}>// dot-pattern-worklet.js
class DotPatternPainter {
static get inputProperties() {
return ['--dot-size', '--dot-color', '--dot-gap', '--dot-opacity']
}
paint(ctx, geometry, properties) {
const size = parseFloat(properties.get('--dot-size')) || 4
const color = properties.get('--dot-color').toString().trim() || '#94A3B8'
const gap = parseFloat(properties.get('--dot-gap')) || 20
const opacity = parseFloat(properties.get('--dot-opacity')) || 1
ctx.globalAlpha = Math.max(0, Math.min(1, opacity))
for (let y = gap / 2; y < geometry.height; y += gap) {
for (let x = gap / 2; x < geometry.width; x += gap) {
ctx.beginPath()
ctx.arc(x, y, size / 2, 0, 2 * Math.PI)
ctx.fillStyle = color
ctx.fill()
}
}
}
}
registerPaint('dot-pattern', DotPatternPainter)
/* CSS での使い方 */
/*
.hero-section {
--dot-size: 3;
--dot-color: #6366F1;
--dot-gap: 24;
--dot-opacity: 0.4;
background-color: #0f0f23;
background-image: paint(dot-pattern);
}
*/>// wavy-border-worklet.js
class WavyBorderPainter {
static get inputProperties() {
return [
'--wave-amplitude',
'--wave-frequency',
'--border-color',
'--border-width',
'--wave-phase', // アニメーション用の位相
]
}
paint(ctx, geometry, properties) {
const amplitude = parseFloat(properties.get('--wave-amplitude')) || 5
const frequency = parseFloat(properties.get('--wave-frequency')) || 0.05
const color = properties.get('--border-color').toString().trim() || '#3B82F6'
const lineWidth = parseFloat(properties.get('--border-width')) || 2
const phase = parseFloat(properties.get('--wave-phase')) || 0
const drawWavyLine = (y) => {
ctx.beginPath()
ctx.moveTo(0, y)
for (let x = 0; x <= geometry.width; x++) {
const waveY = y + Math.sin((x * frequency) + phase) * amplitude
ctx.lineTo(x, waveY)
}
ctx.strokeStyle = color
ctx.lineWidth = lineWidth
ctx.stroke()
}
// 上辺に波線ボーダーを描く
drawWavyLine(amplitude + lineWidth)
// 下辺に波線ボーダーを描く
drawWavyLine(geometry.height - amplitude - lineWidth)
}
}
registerPaint('wavy-border', WavyBorderPainter)>// grid-pattern-worklet.js
class GridPatternPainter {
static get inputProperties() {
return [
'--grid-size',
'--grid-color',
'--grid-major-every', // 何グリッドごとに太線を引くか
'--grid-major-color',
]
}
paint(ctx, geometry, properties) {
const gridSize = parseFloat(properties.get('--grid-size')) || 16
const gridColor = properties.get('--grid-color').toString().trim() || '#e2e8f0'
const majorEvery = parseInt(properties.get('--grid-major-every')) || 4
const majorColor = properties.get('--grid-major-color').toString().trim() || '#cbd5e1'
// 縦線を描く
for (let x = 0; x <= geometry.width; x += gridSize) {
const isMajor = (x / gridSize) % majorEvery === 0
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, geometry.height)
ctx.strokeStyle = isMajor ? majorColor : gridColor
ctx.lineWidth = isMajor ? 1.5 : 0.5
ctx.stroke()
}
// 横線を描く
for (let y = 0; y <= geometry.height; y += gridSize) {
const isMajor = (y / gridSize) % majorEvery === 0
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(geometry.width, y)
ctx.strokeStyle = isMajor ? majorColor : gridColor
ctx.lineWidth = isMajor ? 1.5 : 0.5
ctx.stroke()
}
}
}
registerPaint('grid-pattern', GridPatternPainter)>// noise-texture-worklet.js
// シンプルなValue Noiseを実装(Perlinノイズの簡易版)
class NoiseTexturePainter {
static get inputProperties() {
return ['--noise-scale', '--noise-color', '--noise-opacity', '--noise-seed']
}
// シンプルな疑似ランダム関数(シード値で再現性あり)
random(x, y, seed) {
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.3) * 43758.5453
return n - Math.floor(n)
}
// バイリニア補間
smoothNoise(x, y, seed) {
const ix = Math.floor(x)
const iy = Math.floor(y)
const fx = x - ix
const fy = y - iy
const ux = fx * fx * (3 - 2 * fx) // Hermite smoothstep
const uy = fy * fy * (3 - 2 * fy)
const a = this.random(ix, iy, seed)
const b = this.random(ix + 1, iy, seed)
const c = this.random(ix, iy + 1, seed)
const d = this.random(ix + 1, iy + 1, seed)
return a + (b - a) * ux + (c - a) * uy + (d - a + b + c - a - b - c) * ux * uy
}
paint(ctx, geometry, properties) {
const scale = parseFloat(properties.get('--noise-scale')) || 4
const colorStr = properties.get('--noise-color').toString().trim() || '0,0,0'
const opacity = parseFloat(properties.get('--noise-opacity')) || 0.15
const seed = parseFloat(properties.get('--noise-seed')) || 42
// ピクセルバッファを直接操作(高速)
const imageData = ctx.createImageData(geometry.width, geometry.height)
const data = imageData.data
const [r, g, b] = colorStr.split(',').map(v => parseInt(v.trim()))
for (let y = 0; y < geometry.height; y++) {
for (let x = 0; x < geometry.width; x++) {
const noiseValue = this.smoothNoise(x / scale, y / scale, seed)
const alpha = Math.floor(noiseValue * 255 * opacity)
const idx = (y * geometry.width + x) * 4
data[idx] = r ?? 128
data[idx + 1] = g ?? 128
data[idx + 2] = b ?? 128
data[idx + 3] = alpha
}
}
ctx.putImageData(imageData, 0, 0)
}
}
registerPaint('noise-texture', NoiseTexturePainter)>// progress-bar-worklet.js
// CSSアニメーションでプログレスを動かせるカスタムプログレスバー
class ProgressBarPainter {
static get inputProperties() {
return [
'--progress', // 0〜1の値
'--bar-color',
'--track-color',
'--bar-radius',
'--show-gradient', // グラデーションを使うか
]
}
paint(ctx, geometry, properties) {
const progress = Math.max(0, Math.min(1,
parseFloat(properties.get('--progress')) || 0
))
const barColor = properties.get('--bar-color').toString().trim() || '#3B82F6'
const trackColor = properties.get('--track-color').toString().trim() || '#E2E8F0'
const barRadius = parseFloat(properties.get('--bar-radius')) || geometry.height / 2
const showGradient = properties.get('--show-gradient').toString().trim() === '1'
const { width, height } = geometry
// トラック(背景)を描画
ctx.beginPath()
ctx.roundRect(0, 0, width, height, barRadius)
ctx.fillStyle = trackColor
ctx.fill()
// プログレスバーを描画
const barWidth = width * progress
if (barWidth > 0) {
ctx.beginPath()
ctx.roundRect(0, 0, barWidth, height, barRadius)
if (showGradient && barWidth > 0) {
const gradient = ctx.createLinearGradient(0, 0, barWidth, 0)
gradient.addColorStop(0, barColor + '99')
gradient.addColorStop(1, barColor)
ctx.fillStyle = gradient
} else {
ctx.fillStyle = barColor
}
ctx.fill()
}
}
}
registerPaint('progress-bar', ProgressBarPainter)CSS変数(--my-value)は通常、transitionやanimationによるトゥイーンアニメーションに対応していません。CSS @property(CSS Houdini Properties and Values API)を使って型を定義することで、CSS変数もアニメーション可能になります。
>/* CSS @property でアニメーション可能なカスタムプロパティを定義 */
@property --progress {
syntax: '<number>'; /* 数値型として定義 */
inherits: false; /* 継承しない */
initial-value: 0; /* 初期値 */
}
@property --wave-phase {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
@property --noise-opacity {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
/* プログレスバーのアニメーション */
.progress-bar {
--progress: 0;
--bar-color: #3B82F6;
--track-color: #E2E8F0;
--bar-radius: 4;
--show-gradient: 1;
background-image: paint(progress-bar);
height: 8px;
width: 100%;
/* @propertyで型定義された変数はアニメーション可能! */
transition: --progress 1.5s ease-in-out;
}
.progress-bar.loaded {
--progress: 0.75; /* 0% → 75%に滑らかにアニメーション */
}
/* 波線ボーダーのアニメーション */
@keyframes wave-animation {
from { --wave-phase: 0; }
to { --wave-phase: 6.28318; } /* 2π(1周期) */
}
.wavy-card {
--wave-amplitude: 4;
--wave-frequency: 0.03;
--border-color: #8B5CF6;
--border-width: 2;
--wave-phase: 0;
background-image: paint(wavy-border);
background-color: white;
padding: 2rem;
border-radius: 12px;
animation: wave-animation 2s linear infinite;
}
/* フォールバック(CSS Painting API非対応ブラウザ) */
@supports not (background: paint(x)) {
.progress-bar {
background-color: #E2E8F0;
}
.progress-bar::before {
content: '';
display: block;
height: 100%;
background-color: #3B82F6;
width: calc(var(--progress, 0) * 100%);
border-radius: 4px;
transition: width 1.5s ease-in-out;
}
}>// vite.config.ts でのPaintWorklet対応
import { defineConfig } from 'vite'
export default defineConfig({
// Workletファイルは通常のモジュールと別扱いが必要
build: {
rollupOptions: {
input: {
main: 'index.html',
// Workletは別エントリーとしてビルド
paintWorklet: 'src/worklets/paint-worklet.js',
},
output: {
entryFileNames: (chunkInfo) => {
if (chunkInfo.name === 'paintWorklet') {
return 'worklets/[name].js' // ハッシュなし(URLが変わると困る)
}
return 'assets/[name]-[hash].js'
},
},
},
},
})
// 型付きWorkletの読み込み(main.ts)
async function loadPaintWorklets() {
if (!('paintWorklet' in CSS)) {
// Polyfill を動的にインポート
const { default: polyfill } = await import('css-paint-polyfill')
await polyfill
}
// 本番環境ではビルドされたURLを使用
const workletUrl = import.meta.env.PROD
? '/worklets/paintWorklet.js'
: new URL('./worklets/paint-worklet.js', import.meta.url).href
await CSS.paintWorklet.addModule(workletUrl)
}
loadPaintWorklets().catch(console.error)| ブラウザ | CSS Painting API | @property | Layout API |
|---|---|---|---|
| Chrome 65+ | ✅ | ✅(85+) | ⚠️ 実験的 |
| Edge 79+ | ✅ | ✅(85+) | ⚠️ 実験的 |
| Safari 16.4+ | ❌ | ✅(16.4+) | ❌ |
| Firefox 128+ | ⚠️ 実験的 | ✅(128+) | ❌ |
>/* プログレッシブエンハンスメントのパターン */
/* 1. フォールバックを先に書く */
.element {
background-color: #f3f4f6; /* 全ブラウザで表示されるフォールバック */
background-image: url('pattern-fallback.svg'); /* SVGフォールバック */
}
/* 2. @supports でPainting APIが利用可能なときのみ上書き */
@supports (background: paint(my-painter)) {
.element {
background-image: paint(my-painter); /* 対応ブラウザのみ */
}
}
/* JavaScriptでの対応確認 */
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('./my-worklet.js')
document.documentElement.classList.add('css-paint-supported')
} else {
// css-paint-polyfill で非対応ブラウザをサポート
import('css-paint-polyfill').then(() => {
CSS.paintWorklet.addModule('./my-worklet.js')
})
}
/* CSS側でのフォールバック管理 */
.has-pattern {
background-color: #1e293b; /* デフォルト(未サポート・JS無効) */
}
.css-paint-supported .has-pattern {
background-image: paint(dot-pattern); /* JS有効・API対応時のみ */
}| 比較項目 | CSS Painting API | SVGインライン/data URI | Canvas要素 |
|---|---|---|---|
| ブラウザサポート | Chrome/Edge(部分的) | 全ブラウザ | 全ブラウザ |
| CSS変数連携 | ◎ ネイティブ対応 | △ 一部対応(fill=”var()”) | ❌ 別途JSが必要 |
| パフォーマンス | ◎ ワークレットスレッド | ○ ブラウザ最適化 | △ メインスレッド |
| 動的パラメーター | ◎ CSS変数で動的変更・再描画 | △ SVGの再生成が必要 | ◎ JS直接操作 |
| アニメーション | ◎ @propertyと組み合わせ | ○ CSS animation | ◎ requestAnimationFrame |
| 複雑な描画 | △ 比較的シンプルな描画向け | ◎ 任意のSVG表現 | ◎ ピクセル単位の操作 |
>【使い分けの判断基準】
CSS Painting API を選ぶ場面:
✅ CSSプロパティのbackground-imageとして自然に馴染ませたい
✅ 要素のリサイズに自動追従する背景パターンを作りたい
✅ CSS変数で動的にパラメーターを変更したい
✅ CSSアニメーション(@property)と連携させたい
SVG を選ぶ場面:
✅ 全ブラウザでの完全なサポートが必要
✅ 複雑なベクター表現(曲線・テキスト・フィルタ)
✅ アクセシビリティ(title/desc属性)が必要
✅ SEOインデックスが必要なグラフィック
Canvas APIを選ぶ場面:
✅ ユーザーインタラクションが必要(クリック・ドラッグ)
✅ フレームごとのアニメーション(ゲーム・3D)
✅ ピクセル単位の処理(画像編集)A. PaintWorkletのコンテキストでは import ステートメントは使えません(Workletは独自の制限されたグローバルスコープを持ちます)。外部モジュールを使いたい場合は、ユーティリティ関数をWorkletファイルに直接コピーするか、Viteのビルドを使ってバンドルする必要があります。Viteでは import.meta.url を使ってWorkletファイルを別エントリーとしてビルドし、バンドルされたファイルを CSS.paintWorklet.addModule() で読み込む方法が現実的です。
A. inputPropertiesはCSSカスタムプロパティ(--my-var)の変更を監視してWorkletに渡します。inputArgumentsは paint(painter, arg1, arg2) のようにpaint()関数に直接引数を渡す方法です。inputPropertiesはCSS側で値を管理できて柔軟ですが、inputArgumentsはよりシンプルに値を渡せます。現在のブラウザ実装ではinputPropertiesの方がサポートが安定しているため、inputPropertiesの使用を推奨します。
A. PaintWorkletは通常のJSと異なるコンテキストで動作するため、DevToolsでのデバッグが少し特殊です。Chrome DevToolsでは「Sources」タブの「Threads」セクションに「paint-worklet.js」が表示され、ブレークポイントを設定できます。console.log() はWorklet内でも使用でき、「Console」タブに出力されます。まずconsole.logで中間値を確認しながら開発することを推奨します。
A. @supports (background: paint(x)) {} を使ったプログレッシブエンハンスメントで実装すれば、SafariではCSSフォールバック(通常の背景色や画像)が表示され、Chrome/EdgeではPainting APIの豊かな表現が適用されます。またcss-paint-polyfill(〜8KB)を使えばSafariを含む多くのブラウザでPainting APIをシミュレートできます。装飾的な背景パターンであれば、Safariで少し違う見た目になってもユーザー体験には大きな影響がないため、本番環境での利用は十分現実的です。
A. はい、自動的に再描画されます。CSSのbackground-imageとして機能するため、要素のサイズが変わるたびにpaint()メソッドが新しいgeometry(width/height)で再呼び出しされます。これはSVG背景やCanvas要素と比べた大きなメリットで、レスポンシブレイアウトでもパターンのサイズや密度が適切に自動調整されます。
paint() で呼び出してカスタム描画を実現。CSS Painting APIは「CSSとJavaScriptの境界を溶かす」Houdiniの核心機能です。画像ファイルなしでインタラクティブな背景パターンが実装でき、CSS変数・@property・CSSアニメーションと組み合わせることで非常に豊かな表現が可能になります。Chromeのシェアが高いBtoB・開発者向けサービスには特に効果的です。


副業・フリーランスが主流になっている今こそ、自らのスキルで稼げる人材を目指してみませんか?
未経験でも心配することはありません。初級コースを受講される方の大多数はプログラミング未経験です。まずは無料カウンセリングで、悩みや不安をお聞かせください!
公式サイト より
今すぐ
無料カウンセリング
を予約!