Back

@propertyを使った型付きCSS変数の活用

@propertyを使った型付きCSS変数の活用

CSSの@propertyアットルールは、カスタムプロパティに型を割り当てます。一度登録されると、ブラウザはすべての代入を検証し、アニメーション中に値を補間し、無効な入力に対して定義済みの初期値にフォールバックします。@propertyはCSS変数のバリデーションと補間のギャップを解消しますが、ほとんどの開発者が予期しない形で失敗モードを変化させます。目に見えて壊れた値の代わりに、エラーを一切出さないサイレントフォールバックに置き換えられるのです。

本記事では、3つのディスクリプタとその必須要件、具体例を交えたサポート済み型レジストリ、サイレントフォールバックの挙動とユーザーが実際に目にするもの、一般的なrotationデモを超えた型付きプロパティのアニメーション、JavaScriptの等価APIであるCSS.registerProperty()、現在のブラウザサポート状況、そして登録が手間に見合わないケースの判断基準について解説します。

重要なポイント

  • 型のないCSS カスタムプロパティは文字列です。@propertyはブラウザがすべての代入時に検証する型を付与します。
  • syntaxinheritsディスクリプタは常に必須であり、initial-valuesyntax"*"でない限り必須です。これはCSS Properties and Values API Level 1仕様で定められています。
  • 登録済みプロパティが宣言されたsyntaxに一致しない値を受け取ると、ブラウザはコンソールエラーも視覚的な表示も一切なく、サイレントにinitial-valueへ戻ります。
  • 型付きプロパティはトランジションやアニメーション中に補間されますが、型なしカスタムプロパティは補間されません。ブラウザが2つの不透明な文字列として認識し、数値的な中間点を持たないためです。
  • @propertyは2024年7月9日にBaseline Newly Availableとなりました。2024年以前の資料にある「実験的」という注記はすでに時代遅れです。

問題:型なしカスタムプロパティは単なる文字列

標準のCSSカスタムプロパティは、実際のプロパティに代入されるまで未解析の文字列を保持します。ブラウザは--accentがカラー、長さ、キーワードのいずれを意図しているかを知りません。宣言時にバリデーションを行わず、アニメーション中に2つの値を補間することもできず、意図した使い方に対して値の構造が誤っていても何もフィードバックしません。

3つ目のギャップが実用上の問題です。text-shadowで型なしプロパティを使った例を見てみましょう:

.card {
  --accent: red;
  text-shadow: 4px 2px 5px var(--accent);
}

/* 別の場所で、誤って */
.card {
  --accent: 20px;
}

text-shadowの宣言は代入時に無効となり、シャドウが消えます。--accent20pxに設定された時点では依然として文字列に過ぎないため、その時点では警告が出ません。ブラウザはこのプロパティがカラーであるべきという概念を持っていないのです。MDNカスタムプロパティガイドはこの代入モデルを説明しています:カスタムプロパティの値はvar()で参照されたときにのみ解決されます。

CSS Properties and Values API Level 1仕様の@propertyは、プロパティ自体に型を追加します。一度登録されると、ブラウザは--accent<color>であることを認識し、代入時のみならずすべての代入箇所でそれを強制します。

構文:3つのディスクリプタと必須要件

@propertyアットルールはsyntaxinheritsinitial-valueの3つのディスクリプタを取ります。syntaxinheritsは常に必須です。initial-valuesyntax"*"でない限り必須であり、型付き登録でこれを省略すると@propertyブロック全体が無効となり無視されます。

@property --accent {
  syntax: "<color>";
  inherits: false;
  initial-value: #586de7;
}
  • syntax — 受け入れる型を記述する文字列で、仕様で定義されたサポート済み名称の固定セットから選択します(次のセクションで詳述)。
  • inherits — プロパティがDOMツリーを下って継承されるかどうかを制御するブール値(trueまたはfalse)。これはどのCSSプロパティも持つ継承の挙動と同じです。明示的に設定することで、ネストされたコンポーネント間での型付きプロパティの動作が予測可能になります。
  • initial-value — 他に有効な値が適用されない場合に使用される値であり、無効な入力時にプロパティがフォールバックする値です。

CSS Properties and Values API Level 1仕様 §3.1は要件を正確に定義しています:syntaxまたはinheritsが欠落している場合、@propertyルールは無効です。また、syntaxがユニバーサルの"*"でない限り、initial-valueが欠落している場合も無効です。既存のチュートリアルの中にはinitial-valueを無条件に必須と説明しているものがありますが、仕様ではsyntaxの値に紐付けており、inheritsはこの条件に影響しません。無効な@propertyルールは破棄され、登録は行われず、プロパティは型なしの挙動に戻ります。

CSS @propertyの型レジストリ

syntaxディスクリプタは、CSS Properties and Values API Level 1仕様 §2で定義されたサポート済み構文コンポーネント名を受け入れます。<color><length><percentage><integer><angle><image><custom-ident>などが含まれ、さらに複数値のための乗数(スペース区切りリストには+、カンマ区切りリストには#)や、複数の型を正当に受け入れるプロパティのためのユニオン構文(<color> | <length>)も使用できます。これはCSSの任意の型キーワードへの開かれた扉ではなく、サポート済み名称の固定リストです。

syntaxの値受け入れる値拒否する値initial-valueの例
"<color>"任意の有効なカラー(#f00rebeccapurpleoklch(...)長さ、darkpinkのようなキーワード#586de7
"<length>"pxrememvwなど単位なし数値、パーセンテージ20px
"<percentage>"50%長さ、単位なし数値100%
"<integer>"整数(121.5、長さ12
"<angle>"degradturngrad単位なし数値0deg
"<image>"url(...)、グラデーションカラー、長さurl(bg.png)
"<custom-ident>"作者定義の識別子数値、クォート付き文字列none
"*"任意の値(型なしパススルー)なし — すべてを受け入れる任意

3つの文法拡張により、単一プロパティが受け入れる値の範囲が広がります:

/* "+" — スペース区切りの長さのリスト */
@property --insets {
  syntax: "<length>+";
  inherits: false;
  initial-value: 0px;
}

/* "#" — カンマ区切りのカラーのリスト */
@property --stops {
  syntax: "<color>#";
  inherits: false;
  initial-value: black;
}

/* "|" — ユニオン:長さまたはキーワード"auto"を受け入れる */
@property --gap {
  syntax: "<length> | auto";
  inherits: false;
  initial-value: auto;
}

仕様のサポート済み構文文字列セクションによると、+乗数はスペース区切りリストを、#はカンマ区切りリストを意味します。|ユニオンにより、プロパティが複数の型を受け入れることが可能になります。例えば、長さまたはキーワードを受け入れるプロパティに便利です。"*"ユニバーサル構文は型チェックを完全に無効化します。デフォルトとなる型がないため、initial-valueが任意となる唯一のケースです。型ごとの定義については、MDN CSS値型リファレンスCSSWG型インデックスで各コンポーネント名を確認できます。

バリデーション:予期しないサイレントフォールバック

登録済みプロパティが宣言されたsyntaxに一致しない値を受け取ると、ブラウザはその代入を破棄し、initial-valueを使用して要素をレンダリングします。現在のブラウザでは、このフォールバックはコンソールエラーも、レンダリングされたページ上の視覚的な表示も一切生成しません。ページは壊れませんが、何か問題が起きたことも教えてくれません。

@property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 90deg;
}

.card {
  --hue: 220deg;            /* ✅ 有効、使用される */
  --hue: #f00;              /* ❌ 型が無効、無視される — 90degに戻る */
  background: oklch(70% 0.15 var(--hue));
}

backgroundは常に有効なカラーに解決されます。無効な代入の後、--hue#f00にも空にもなりません。無効な値は破棄され、プロパティは登録されたinitial-value90degに解決されます。MDNの@propertyページでは、これを「計算値の時点で無効」となり、登録された初期値に解決されると説明しています。

これは目に見えてレイアウトが壊れるよりも確かに優れています。しかし同時に、新たな失敗クラスでもあります。型なしカスタムプロパティは目立って失敗します。依存する宣言が壊れ、目で確認できます。型付きプロパティはサイレントに失敗します:JSのテーマスイッチャーが不正なカラーを書き込んだり、ユーザー提供の値が解析できなかったり、デザイントークンに誤った単位が付いたりしても、コンポーネントはエラーの痕跡なくデフォルト状態でレンダリングされます。要素を直接インスペクトすればDevToolsで計算されたフォールバック値を確認できますが、実行時にコンソールには何も表示されません。

これはセッションリプレイが捕捉するために設計されたバグのクラスです。型付きカスタムプロパティが本番環境で無効な入力(ユーザー入力、設定ミスのトークン、実行時のテーマ変更など)を受け取った場合、ブラウザはサイレントにinitial-valueにフォールバックし、JavaScriptエラーは発火せず、標準的なエラー監視はシグナルを生成しません。デプロイ後の唯一の証拠は視覚的なものです:誤った色やサイズでレンダリングされたコンポーネントです。このような実装のセッションリプレイは、コンソールのみのツールでは何も見えない状況で、不正な値が代入された瞬間のレンダリングされたDOMを直接捉えることで、問題の状態を明らかにすることが多いです。

アニメーション:型付きプロパティの補間

登録済みカスタムプロパティはアニメーション中に補間されますが、未登録のものは代わりに離散的にアニメーションします。これが型付けの最も有用な結果です。ブラウザが--hueを文字列ではなく<angle>として理解するため、トランジション中に0degから360degへの補間が可能になります。これは型なしカスタムプロパティでは不可能なことで、ブラウザが数値的な中間点を持たない2つの不透明な文字列として認識してしまうためです。CSSトランジション仕様では、補間は型付き値に対して動作すると定義しています。未登録のカスタムプロパティには型がないため、ブラウザはトゥイーンする代わりに離散的に切り替えます。

他のチュートリアルはすべてtransform: rotate()でこれを実演しています。より説得力のあるケースとして、oklch()カラーのhueチャンネルをアニメーションさせる例を示します。これは型付けにより、スタンドアロンプロパティだけでなく関数内の値も補間できることを示しています:

@property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.swatch {
  width: 200px;
  height: 200px;
  border-radius: 12px;
  background: oklch(65% 0.2 var(--hue));
  animation: hue-cycle 6s linear infinite;
}

@keyframes hue-cycle {
  to {
    --hue: 360deg;
  }
}

ブラウザが--hue0degから360degにトゥイーンし、各フレームでoklch(65% 0.2 var(--hue))を再計算するため、スウォッチはフルのhueホイールを滑らかに循環します。CSS Color Level 4仕様では、oklch()のhue引数が<angle>を受け入れると定義しており、これはまさに登録した型と一致します。@propertyブロックを削除するとアニメーションが壊れます:--hueが型なし文字列になり、ブラウザが補間できなくなり、スウォッチは循環する代わりに開始から終了へとスナップします。このビフォー/アフターが、モーションにおいて登録が重要な理由を最も明確に示しています。

JavaScript等価API:CSS.registerProperty()

CSS.registerProperty()@propertyアットルールの命令型の等価APIです。JavaScriptから実行時に型付きカスタムプロパティを登録し、namesyntaxinherits、オプションのinitialValueを持つオブジェクトを受け取ります:

window.CSS.registerProperty({
  name: "--hue",
  syntax: "<angle>",
  inherits: false,
  initialValue: "0deg",
});

CSS APIではハイフン付きのinitial-valueディスクリプタであるのに対し、JS APIではキャメルケースのinitialValueであることに注意してください。MDNのCSS.registerProperty()リファレンスにパラメータ名と挙動が記載されています。2つの登録方法は効果として等価であり、どちらの方法で登録されたプロパティも同様に型付けされ検証されます。

デフォルトではアットルールを使用してください。スタイルの他の部分と共存し、宣言的であり、有効化にJavaScriptの実行を必要としません。登録が動的である必要がある場合、つまりsyntaxinitialValueが実行時の条件に依存する場合や、初期化の一部としてプログラム的にプロパティを登録するライブラリの場合はCSS.registerProperty()を使用してください。CSS.registerProperty()で登録されたプロパティは再登録できないため、2回実行されないよう注意してください。

ブラウザサポート

2024年7月9日時点で、@propertyはBaseline Newly Availableとなり、Chrome、Firefox、Safariの現行バージョンでサポートされています。古い資料の「実験的」という注記はすでに時代遅れです。Firefoxはバージョン128でサポートを追加し、2024年7月にリリースされたことでクロスブラウザサポートが完成しました。Safariは16.4でリリース済みで、Chromeはバージョン85からサポートしています。web.devのBaselineアナウンスで日付とステータスが確認できます。正確なバージョンデータはcaniuseを参照してください。2024年半ば以前に公開されたチュートリアルでは、サポートが「実験的」または「近日公開予定」と説明されていますが、それらの記述はもはや有効ではありません。

実際の使用パターン

@propertyの最も価値の高い使用例には共通の特徴があります:プロパティがアニメーションするか、外部入力を受け取るか、明示的な継承制御が必要なケースです。

グローバル定義、スコープ付き利用

@propertyブロックをグローバルなトークンレイヤーに一度定義し、利用するコンポーネントは通常通りvar()で変数を参照します。参考チュートリアルの多くは@propertyをそれを使用するルールのすぐ上に宣言していますが、これはデザインシステムの作業には誤解を招きます。実際のパターンは登録と利用を分離します:

/* tokens.css — ドキュメントルートで一度読み込まれる */
@property --brand-hue {
  syntax: "<angle>";
  inherits: true;
  initial-value: 250deg;
}

@property --surface {
  syntax: "<color>";
  inherits: true;
  initial-value: #1a1a1a;
}
/* card.css — コンポーネントは登録を参照しない */
.card {
  background: var(--surface);
  border-color: oklch(60% 0.1 var(--brand-hue));
}

--surfaceへのすべての代入(テーマスイッチャー、メディアクエリ、ユーザー入力から)が検証されます。inherits: trueを設定することで、トークンが子孫要素にカスケードされます。

アダプティブサーフェスによるカラーテーマ

型付きカラートークンにより、単一の--brand-hueoklch()を通じてサーフェスパレットを駆動できます。不正なhue値はパレットを壊す代わりに登録された初期値にフォールバックします:

html:has(#dark:checked) {
  --surface: oklch(20% 0.1 var(--brand-hue));
}
html:has(#light:checked) {
  --surface: oklch(95% 0.04 var(--brand-hue));
}

スクロール駆動プログレスインジケーター

型付きの<percentage>または<length>はプログレス値として明確に読み取れ、アニメーションで駆動されるかJavaScriptから更新される場合に滑らかに補間されます:

@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.progress-bar {
  width: var(--progress);
  transition: width 0.2s linear;
}
function onScroll() {
  const pct = (scrollY / (document.body.scrollHeight - innerHeight)) * 100;
  document.querySelector(".progress-bar")
    .style.setProperty("--progress", `${pct}%`);
}

--progress<percentage>として型付けすることで、誤ったパーセンテージ以外の値はwidthを破損する代わりに0%に戻ります。

@propertyを使わないケース

アニメーションしない、外部入力を受け取らない、明示的な継承制御が不要なカスタムプロパティには@property登録をスキップしてください。純粋に静的なトークンに対しては、3つのディスクリプタは実行時のメリットなしに構文上のオーバーヘッドを追加するだけです。登録が正当化されるのは、以下の3つの条件のうち少なくとも1つが当てはまる場合です:

  1. 値がアニメーションまたはトランジションする。 補間には登録された型が必要です。
  2. 値が無効になりうる外部入力を受け取る。 テーマスイッチャー、ユーザー入力、または不正な形式になりうるビルド時トークンは、サイレントフォールバック保証の恩恵を受けます。
  3. 継承の挙動を明示的に制御する必要がある。 ネストされたコンポーネント間でプロパティのカスケード動作をロックする必要がある場合。

一度設定されてアニメーションも外部入力もない静的なスペーシングスケール、z-indexの階層、font-family文字列には、:root内のプレーンなカスタムプロパティの方がシンプルで十分です。そこに@propertyを追加しても、維持すべき3つのディスクリプタが増えるだけで、そうしなければ得られない挙動は何もありません。アニメーションするか入力を受け取るプロパティを型付けし、残りは文字列のままにしておきましょう。

型付きカスタムプロパティはブラウザをバリデーターと補間エンジンに変えますが、その代償は静かな失敗モードです:無効な入力はエラーの痕跡なくinitial-valueに戻ります。アニメーションするか実行時入力を受け取るプロパティを登録し、適切な初期値を設定し、そのフォールバックを問題を隠す安全網としてではなく、本番環境で監視すべき挙動として扱ってください。

よくある質問

@propertyアットルールはトップレベルのアットルールであり、:rootやセレクターの中にネストするのではなく、単独で宣言します。スタイルシートの任意の場所に@propertyブロックを記述して型をグローバルに登録し、プロパティの値は:rootや任意のセレクター内で通常のカスタムプロパティと同様に設定します。登録は値が後でどこに代入されるかに関係なくドキュメント全体に適用されるため、グローバルなトークンファイルに@propertyを配置し、:rootで値を代入するのが標準的なパターンです。

どちらも同一の実行時挙動で型付きカスタムプロパティを登録しますが、@propertyはJavaScriptなしで有効になる宣言的なCSSであるのに対し、CSS.registerProperty()は実行時に命令的に実行されます。スタイルと共存しスクリプトの実行を必要としないため、デフォルトでは@propertyを使用してください。syntaxやinitialValueが実行時の条件に依存する場合など、登録が動的である必要がある場合にのみCSS.registerProperty()を使用してください。CSS.registerProperty()はキャメルケースのinitialValueを使用し、プロパティを再登録できず、同じ名前で2回呼び出すと例外をスローすることに注意してください。

できません。型なしカスタムプロパティは不透明な文字列として保存されるため、ブラウザは数値的な中間点を持たない2つの文字列として認識し、補間する代わりに離散的に値を切り替えます。@propertyでプロパティを登録するとブラウザが理解できる型が付与され、トランジションとキーフレームにわたる補間が可能になります。例えば、未登録の角度は開始から終了へとスナップしますが、<angle>として登録された同じプロパティは滑らかにトゥイーンします。補間を可能にするのは型の登録です。

ブラウザは無効な代入を破棄し、登録されたinitial-valueを使用して要素をレンダリングします。これはコンソールエラーも、フォールバックが発動したことを示すレンダリングページ上の視覚的な表示も一切なくサイレントに発生します。仕様ではこの挙動を「計算値の時点で無効」となり登録された初期値に解決されると説明しています。ページは壊れませんが、何か問題が起きたことも通知されません。そのため、これらのリグレッションは標準的なエラー監視では見えなくなります。DevToolsは要素を直接インスペクトした場合にのみ計算されたフォールバック値を表示します。

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..

OpenReplay