カスタムプロパティ(CSS変数)に色を入れて transition を書いたのに、じわっと変わらずパッと切り替わってしまった——この挙動には理由があります。ブラウザーにとって --main-color はただの文字列で、#2f6fed と #e0483c の中間の色を計算する手がかりが無いからです。@property を使うと、カスタムプロパティに「これは色です」「これは長さです」と型を教えられ、アニメーションできるようになります。この記事では @property の書き方と3つの記述子、型を付けることで得られる利点を、動かせるデモ付きで解説します。
目次
カスタムプロパティは既定では「ただの文字列」
--main-color: #2f6fed; と書いたとき、ブラウザーはこれを色として理解しているわけではありません。--main-color の値は #2f6fed という文字の並びとして保持され、var(--main-color) で参照された場所に貼り付けられて、そこではじめて色として解釈されます。仕様上このような値は「<declaration-value>」、つまり何でも入る型として扱われます。
この設計は自由度が高い反面、困ることがあります。トランジションやアニメーションは「開始値と終了値の中間」を計算する仕組みですが、文字列のままでは中間が作れません。結果として、値が切り替わる瞬間にいきなり別の値になります。@property は、この前提を変えるための仕組みです。
@property の書き方
@property はアットルールで、登録したいカスタムプロパティ名をそのまま続けて書きます。
@property --main-color {
syntax: "<color>"; /* 受け付ける値の型 */
inherits: false; /* 子要素に継承するか */
initial-value: #2f6fed; /* 初期値 */
}
書ける記述子は3つだけです。
| 記述子 | 内容 | 必須 |
|---|---|---|
syntax | 受け付ける値の型。文字列として引用符で囲む | 必須 |
inherits | true なら子要素に継承する。false なら継承しない | 必須 |
initial-value | 値が指定されていないときに使われる初期値 | syntax が "*" のときを除き必須 |
1つでも欠けると、その @property ルール全体が無視されます。型を付けたつもりがアニメーションしないときは、まず inherits の書き忘れを疑ってください。通常のプロパティのように「省略したら既定値」とはならず、丸ごと捨てられる点が独特です。
syntax に書ける型
よく使うものを挙げます。
| syntax | 受け付ける値の例 |
|---|---|
"<color>" | #2f6fed / rgb(0 0 0) / red |
"<length>" | 16px / 2rem / 50vw |
"<percentage>" | 40% |
"<length-percentage>" | 16px と 40% のどちらも |
"<number>" | 1.5 / 0 |
"<integer>" | 3(小数は不可) |
"<angle>" | 45deg / 0.5turn |
"<time>" | 0.3s / 200ms |
"<image>" | linear-gradient(...) / url(...) |
"*" | 何でも(型チェックもアニメーションもしない) |
複数の型を許したいときは | でつなぎます。特定のキーワードだけを許すこともできます。
/* 長さ または auto キーワードを受け付ける */
@property --gap {
syntax: "<length> | auto";
inherits: false;
initial-value: 16px;
}
/* 決まった3つの単語だけ受け付ける */
@property --tone {
syntax: "light | dark | auto";
inherits: true;
initial-value: auto;
}
/* 空白区切りのリスト(+)。カンマ区切りなら # を使う */
@property --shadow-offsets {
syntax: "<length>+";
inherits: false;
initial-value: 0px 0px;
}
型を付けるとアニメーションできる
実際の違いを見てみましょう。左のカードは @property で登録した変数、右は登録していない変数を使っています。どちらも同じように transition を書いていますが、挙動がまったく違います。
<div class="cards">
<div class="card registered">
<span>@property で登録した<br>--tint(ホバーしてみてください)</span>
</div>
<div class="card plain">
<span>登録していない<br>--plain</span>
</div>
</div>
/* --tint を「色」としてブラウザーに登録する */
@property --tint {
syntax: "<color>";
inherits: false;
initial-value: #2f6fed;
}
.cards {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.card {
flex: 1 1 200px;
height: 130px;
display: grid;
place-items: center;
border-radius: 10px;
color: #fff;
font-family: sans-serif;
font-size: 14px;
line-height: 1.7;
text-align: center;
}
/* 登録済み:途中の色を計算できるので、なめらかに変化する */
.registered {
background: linear-gradient(135deg, var(--tint), #7fb0ff);
transition: --tint 0.6s ease;
}
.registered:hover {
--tint: #e0483c;
}
/* 登録なし:ただの文字列なので中間の色が作れず、一瞬で切り替わる */
.plain {
--plain: #2f6fed;
background: linear-gradient(135deg, var(--plain), #7fb0ff);
transition: --plain 0.6s ease;
}
.plain:hover {
--plain: #e0483c;
}
左はグラデーションの色がなめらかに変わり、右は一瞬で切り替わります。@property で syntax: "<color>" と宣言したことで、ブラウザーが --tint を色として保持するようになり、青と赤の中間の色を計算できるようになったためです。
これは @keyframes でも同じです。次のデモは、conic-gradient() の開始角度をアニメーションさせて枠線を回しています。グラデーション全体をアニメーションさせることはできませんが、その中で使っている角度の変数だけを動かすことで実現できます。
<div class="badge">
<div class="badge__inner">
<strong>@property</strong>
<p>角度をアニメーションさせた枠線</p>
</div>
</div>
/* --angle を「角度」として登録する。これが無いと回らない */
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.badge {
padding: 4px;
border-radius: 14px;
/* conic-gradient の開始角度に変数を使う */
background: conic-gradient(
from var(--angle),
#2f6fed,
#a855f7,
#ec4899,
#f59e0b,
#2f6fed
);
animation: spin 3s linear infinite;
}
@keyframes spin {
to {
--angle: 360deg;
}
}
.badge__inner {
background: #fff;
border-radius: 11px;
padding: 24px;
text-align: center;
font-family: sans-serif;
}
.badge__inner p {
margin: 6px 0 0;
font-size: 14px;
color: #555;
}
@keyframes spin の中で変化させているのは --angle だけです。background そのものは conic-gradient(from var(--angle), ...) のまま固定で、変数の値が変わるたびにグラデーションが引き直されています。@property --angle の宣言を消すと、この回転はぴたりと止まります。
アニメーション以外の効果
不正な値が入っても壊れない
型を宣言すると、その型に合わない値は登録時点ではじかれ、初期値が使われます。JavaScript から値を書き込むような場面で効いてきます。
@property --card-radius {
syntax: "<length>";
inherits: false;
initial-value: 8px;
}
.card {
/* 誤って単位のない値を入れてしまった */
--card-radius: 12;
border-radius: var(--card-radius);
}
登録していなければ border-radius: 12 という不正な宣言になり、角丸が消えます。登録していれば 12 は <length> でないと判定され、初期値の 8px が使われます。壊れるのではなく、決めておいた値に戻るわけです。
継承するかを選べる
通常のカスタムプロパティは必ず子要素に継承されます。inherits: false にすると継承が止まり、値を設定していない要素では初期値が使われます。
使い分けの目安として、テーマカラーのようにページ全体へ配りたい値は true、その要素だけで完結する値は false にします。上の --angle のようなアニメーション用の変数は、子要素まで巻き込むと予想外の場所が動き出すので false が適切です。
JavaScript から登録する
同じ登録は CSS.registerProperty() でも行えます。CSS ファイルを触らずにライブラリ側で型を定義したい場合などに使います。
CSS.registerProperty( {
name: '--main-color',
syntax: '<color>',
inherits: false,
initialValue: '#2f6fed',
} );
キー名が initial-value ではなく initialValue になる点に注意してください。同じ名前で二度登録すると InvalidModificationError が投げられるので、繰り返し実行されうる場所では try で囲むか、登録済みかどうかを管理する必要があります。CSS で書けるならそちらのほうが安全です。
登録したのに動かないとき
記述子が足りていない
繰り返しになりますが、syntax と inherits は必須、initial-value も syntax: "*" 以外では必須です。inherits を書き忘れているケースがとにかく多いので、動かないときは最初にここを見てください。ルールごと無視されるため、開発者ツールの Styles パネルにも出てこなくなります。
syntax の引用符を忘れている
syntax: <color>; は不正です。syntax: "<color>"; のように文字列として引用符で囲む必要があります。initial-value のほうは引用符を付けません。
変数ではなくプロパティをトランジションに書いている
色を変えたいからといって transition: background 0.6s と書いても、background がグラデーションの場合は補間できません。動かしたいのは変数のほうなので、transition: --tint 0.6s のようにカスタムプロパティ名をそのまま書きます。transition: all でも登録済みのカスタムプロパティは対象になります。
単位のない 0 を初期値にしている
syntax: "<length>" に対して initial-value: 0; と書くと、単位が無いため型に合わず登録に失敗します。通常の CSS では 0 に単位が要りませんが、ここでは 0px と書いてください。角度なら 0deg、時間なら 0s です。
まとめ
@property は、カスタムプロパティに型・継承の有無・初期値を宣言するためのアットルールです。型が付くとブラウザーが値の中身を理解できるようになり、transition や @keyframes で滑らかに変化させられます。グラデーションの色や角度のように、これまで CSS 単体では動かせなかった表現がこれで書けるようになります。
書くべき記述子は syntax・inherits・initial-value の3つで、1つでも欠けるとルールごと無視されます。syntax は引用符で囲む、長さの初期値は 0px のように単位を付ける、この2点を押さえておけば登録で詰まることはほとんどありません。
アニメーション以外にも、不正な値が入ったときに初期値へフォールバックする、継承を止められる、といった利点があります。まずは手元のホバーエフェクトで、色のカスタムプロパティを1つ @property で登録してみてください。transition に変数名を書き足すだけで、切り替わっていた色がすっとつながるようになります。