CSS Houdiniの活用:通常のCSSでは実現できない独自エフェクトの作成
CSS Houdiniの活用:通常のCSSでは実現できない独自エフェクトの作成
一見単純に思えることを試してみてください。conic-gradient()の角度をホバー時に滑らかにアニメーションさせる、というものです。カスタムプロパティ--kat: 0degを定義し、transition: --kat 0.4sを追加して、マウスを乗せてみる——そして何も起きません。グラデーションはtransitionなど存在しないかのように、カクカクと切り替わります。これはあなたのコードのバグではありません。ブラウザにとって--katはただの不透明な文字列であり、それが角度であることを知らないため、0degと180degの間を補間する術がないのです。ブラウザは数値、色、長さなら補間できます——それらの型を知っているからです。カスタムプロパティは、CSSに組み込まれたプロパティとは違い、いかなる型も持ちません。まさにこの隙間にCSS Houdiniが入り込みます。
Houdiniとは本当は何なのか
よくある誤解ですが、Houdiniは単一の技術でも、単一の仕様でもありません。これはW3Cにおけるいくつかの独立した提案を束ねる傘のような存在であり、それらを結び付けているのは一つのアイデアです——それまで開発者から完全に隠されていたレンダリングエンジンの各部分(値のパース、レイアウト、描画、アニメーション)を露出させる、というものです。CSSに欲しい機能そのもののプロパティが登場するのを待つのではなく、その処理過程の特定の段階に対する低レベルなフックを手に入れられるのです。
これらのフックはworkletとして実装されています——Web Workerに概念的に近いものの、レンダリングパイプライン専用に設計された小さなJavaScriptモジュールです。workletはメインスレッドの外で動作し、window、document、DOMへのアクセスを持たず、厳密に定義された契約(例えばpaint(ctx, size, properties))を通じてのみやり取りします。これは仕様策定者の怠慢による制約ではなく、意図的なアーキテクチャ上の判断です。この分離によって、エンジンはworkletを別々のスレッド上で並列に実行し、あなたのコードが触れるべきでないものに触れるリスクなしに結果をキャッシュできます。同じ分離の仕組みにより、worklet内にはDateもperformance.now()もfetchもありません——高精度な時刻情報やネットワークへのアクセスを制限するのは、ページを描画するコードのレベルからタイミング攻撃やフィンガープリンティングを行われないようにするための、意図的な防御策です。
この傘の下にある個々の部分は、今日ではまったく異なる段階にあります。あるものは長らくBaselineの一部であり、別のものはChromeでしか動かず、さらに別のものは10年経った今も実験段階のままです。この区別は非常に重要です——そしてHoudiniを紹介する記事の多くはこの区別を無視し、全体をひとまとめの、すぐ使える技術であるかのように扱っています。
今日、実際に使う価値のある唯一のHoudini:@property
CSS Properties and Values APIは、まさにこの記事の冒頭で挙げた問題を解決します。カスタムプロパティを、具体的な型(syntax)、初期値、そして継承させるかどうかの情報とともに登録すると、その瞬間からブラウザはそれを文字列ではなく、れっきとしたアニメーション可能な値として扱うようになります。
html
<div class="ring"></div>
css
@property --kat {syntax: '<angle>';inherits: false;initial-value: 0deg;}.ring {width: 200px;height: 200px;border-radius: 50%;background: conic-gradient(from var(--kat), #ff4d4d, #4d79ff, #4dff88, #ff4d4d);transition: --kat 0.6s ease;}.ring:hover {--kat: 180deg;}
この例は、通常のCSSでは実現できないことを実際に行っています——@propertyがなければ、上記のtransitionは記事冒頭のシナリオとまったく同じように機能しなかったはずです。syntax: '<angle>'は、--katが角度であることをブラウザに伝え、それによって滑らかな補間が可能になります。inherits: falseは子要素への意図しない継承を防ぎますが、これはカスタムプロパティにおいて発見しづらいバグの原因になりがちな点です。
同じ効果はJavaScriptからも実現できます。これは、プロパティをスタイルシートに静的に書くのではなく、(例えばデータに基づいて生成するなど)動的に登録する場合に理にかなっています。
javascript
if ('registerProperty' in CSS) {CSS.registerProperty({name: '--kat',syntax: '<angle>',inherits: false,initialValue: '0deg',});}
これは今日のHoudiniの中で唯一Baselineのステータスを持つ部分であり、Chrome、Firefox(バージョン128以降)、Safari(16.4以降)でネイティブに動作し、polyfillも@supportsも必要ありません。Houdini全体からたった一つだけ覚えて導入するとしたら、まさにこれです。
CSS Paint API:本当に描画するworklet
Paint APIを使うと、backgroundをpaint()関数に置き換えることができます。この関数は、Canvas 2Dに似た縮小版インターフェース(PaintRenderingContext2D)を使って要素上に直接描画します——ただしfillText、drawImage、ピクセルの読み取りはありません。この方法が通常の<canvas>への描画と異なるのはinputPropertiesです。workletはどのカスタムプロパティを監視したいかを宣言し、そのいずれかが変化したときだけ自動的に再描画されます——イベントを監視するためのJavaScriptコードは一行も必要ありません。
以下は、警告用の斜めストライプの例です。その角度は、先ほど登録したのと同じ仕組みのプロパティ(正確にはそのローカル版である--stripe-angle)によって制御されており、@propertyとPaint APIがいかに自然に補完し合うかを示しています。
html
<div class="hazard"></div>
css
@property --stripe-angle {syntax: '<angle>';inherits: false;initial-value: 45deg;}.hazard {width: 100%;height: 120px;--stripe-angle: 45deg;background: paint(hazardStripes);transition: --stripe-angle 0.4s ease;}.hazard:hover {--stripe-angle: 135deg;}
javascript
// main.jsif ('paintWorklet' in CSS) {CSS.paintWorklet.addModule('hazard-stripes.js');}
javascript
// hazard-stripes.jsclass HazardStripesPainter {static get inputProperties() {return ['--stripe-angle'];}paint(ctx, size, properties) {const angle = properties.get('--stripe-angle').to('deg').value;const stripeWidth = 24;const { width, height } = size;const diagonal = Math.sqrt(width ** 2 + height ** 2) * 2;ctx.save();ctx.translate(width / 2, height / 2);ctx.rotate((angle * Math.PI) / 180);ctx.translate(-diagonal, -diagonal);let stripeIndex = 0;for (let x = 0; x < diagonal * 2; x += stripeWidth) {ctx.fillStyle = stripeIndex % 2 === 0 ? '#111111' : '#f5c400';ctx.fillRect(x, 0, stripeWidth, diagonal * 2);stripeIndex++;}ctx.restore();}}registerPaint('hazardStripes', HazardStripesPainter);
注目すべきはproperties.get('--stripe-angle').to('deg').valueの部分です——これはCSS Typed OM、同じAPIファミリーに属する別の要素です。文字列を手動でパースする(parseFloatで"deg"を切り落とすなど)代わりに、型付きのCSSUnitValueを受け取り、必要な単位へ明示的に変換します。描画されるストライプの角度が、inputPropertiesで宣言されたカスタムプロパティのみに依存しているおかげで、エンジンは要素をいつ再描画すべきかを正確に把握できます——ページ上の他の場所で無関係なCSSプロパティが変化しても、paint()が再実行されることはありません。
Layout APIとAnimation Worklet:2026年の現実
ここからは、Houdiniを紹介する記事の多くが触れない部分です。CSS Layout API——同じ仕様の3本目の柱であり、独自のレイアウトアルゴリズム(自作のdisplay: gridに相当するもの)を定義できるもの——は2015年からプロジェクトとして存在していますが、10年経った今も実験段階のままです。安定版のブラウザでネイティブに実装したものは一つもなく、利用できるのはフラグを有効にした場合か、Chromeのorigin trialのみです。もしdisplay: layout(名前)をすぐ使える解決策として紹介する記事を見かけたら、公開日を確認してください——それはおそらく数年前のorigin trialのデモであり、今日ユーザーのブラウザで動かせるものではありません。
Animation Workletは、メインスレッドの外で実行される、スクロール駆動アニメーションの完全な制御を提供するはずのものでした。実際にサポートしているのはChromiumだけです。さらに重要なのは、その主要なユースケースがもはやHoudiniに頼る理由ではなくなったということです。CSSが同じ問題に対するネイティブな解決策を手に入れたからです。animation-timeline: scroll()を使えば、今日ではJavaScriptを一行も書かず、workletも使わずにアニメーションをスクロールと連動させることができ、ブラウザのサポートも広がりつつあります。これは、より大きなパターンをよく示す例です。Houdiniがそもそも生まれた理由の一部は、時間が経つにつれて、開発者向けの低レベルAPIとして残るのではなく、CSS仕様そのものに直接取り込まれていったのです。
実務でいつ意味を持つか
@propertyは迷わず、保護なしで使う – Baselineのステータスを持ち、@supportsもpolyfillも不要です。- Paint APIはプログレッシブエンハンスメントとして扱う –
@supports(background: paint(x))で包み、Firefox向けには純粋なCSSによる妥当なフォールバック(例えば通常のグラデーション)を用意するか、効果をどこでも動かす必要がある場合はpolyfillのcss-paint-polyfillを利用しましょう。 - Layout APIを前提に本番機能を計画しない – これはまだブラウザの実質的なサポートを欠く実験であり、遊ぶには良くても、ロードマップに載せるものではありません。
paint()は「無料の」CSSではないことを忘れない – これはあなたのJavaScriptコードであり、宣言したinputPropertiesが変化するたびに再実行されます。コストの高い計算(ノイズ生成、複雑な手続き的パターンなど)は、<canvas>のレンダリングループとまったく同じように最適化する必要があります——うっかりすると、レイアウトのメインスレッド上で要素全体をフレームごとに再描画するアニメーションを作ってしまいがちです。
まとめ
Houdiniは「使うか使わないか」という一つの決断ではなく、リスクの異なる三つ、四つの独立した決断です。第一に、@propertyは現実の具体的な問題(カスタムプロパティのアニメーション化)を解決し、今日では他のどのCSSプロパティとも同じくらい安全に使えます——このエコシステムの中で、ためらわずに導入する価値があるのはこの部分だけです。第二に、Paint APIは実質的な力を与えてくれます(宣言された依存関係が変化したときだけ再描画されるworkletを、イベントを手動で監視することなく使える)が、サポートしていないブラウザに対する意識的な計画が必要です——これはプログレッシブエンハンスメントのための道具であり、クリティカルな機能のためのものではありません。第三に、Layout APIとAnimation Workletは、仕様そのものが採用を保証するわけではないことを示す良い例です——何年も経った今、一方はいまだ実装を待ち続け、もう一方はよりシンプルでネイティブなCSSに部分的に置き換えられました。Houdiniに手を伸ばす前に、必要としている部分が、これら三つのバスケットのどれに正確に当てはまるのかを確認してください。