PropFLIP:@property 4つと transition でレイアウト変化を FLIP アニメーションする
FLIP の「Invert」を @property で型付けした4つのカスタムプロパティ(--tx --ty --sx --sy)に入れ、「Play」を CSS の transition に任せると、途中で割り込める・軸ごとにイージングを変えられるレイアウトアニメーションになる。
どんな時に使う?
カードがグリッドの別のセルへ移る、リストが並び替わる、箱が大きくなる、といった「レイアウトが変わる」動きを滑らかに見せたいとき。top/left/width/height を transition すると毎フレームレイアウトが走って重い。transform は軽いが「どこへ行くか」はレイアウト次第なので、変化前後の位置を JS で測って差分を transform で埋める(FLIP)のが定番。PropFLIP はその差分を @property に入れることで、アニメーション部分を CSS 側に寄せる。
- アニメーション中にもう一度動かしても自然につながる(transition なので割り込み可能)
- x・y・横スケール・縦スケールに別々の duration / easing / delay を付けられる(x と y のタイミングをずらすと弧を描く動きになる)
- 角丸と中身の文字が伸び縮みしない補正ができる
コード
CSS
css
/* 4つの型付きプロパティ。スケールは子の補正に使うので inherits: true */
@property --tx { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --ty { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --sx { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --sy { syntax: "<number>"; inherits: true; initial-value: 1; }
.flip {
--r: 16px; /* 角丸 */
--p: 16px; /* padding */
padding: var(--p);
transform:
translate(calc(var(--tx) * 1px), calc(var(--ty) * 1px))
scale(var(--sx), var(--sy));
transform-origin: top left; /* getBoundingClientRect() の基準に合わせる */
transition: --tx, --ty, --sx, --sy;
transition-duration: .6s, 1s, .4s, .4s; /* プロパティごとに変えられる */
transition-timing-function: ease-out, cubic-bezier(.3, 1.5, .5, 1), ease, ease;
/* 角丸がスケールで楕円にならないよう割り戻す */
border-radius: calc(var(--r) / var(--sx)) / calc(var(--r) / var(--sy));
}
/* 直下の子だけ逆スケールで文字の伸びを打ち消す(孫はそのまま付いてくる) */
.flip > * {
transform-origin: top left;
transform:
translate(calc(var(--p) / var(--sx) - var(--p)),
calc(var(--p) / var(--sy) - var(--p)))
scale(calc(1 / var(--sx)), calc(1 / var(--sy)));
}JS(FLIP の手順)
js
function animateLayout(el, updateDOM) {
const first = el.getBoundingClientRect(); // F: 変化前を測る
el.style.transition = 'none'; // いったん transition を止める
updateDOM(); // DOM を変える
const last = el.getBoundingClientRect(); // L: 変化後を測る
// I: 差分(位置は引き算、大きさは割り算)を入れて見た目を元の位置に戻す
el.style.setProperty('--tx', first.left - last.left);
el.style.setProperty('--ty', first.top - last.top);
el.style.setProperty('--sx', first.width / last.width);
el.style.setProperty('--sy', first.height / last.height);
el.offsetHeight; // スタイルを確定させる(これがないと動かない)
el.style.transition = ''; // P: transition を戻して 0/1 へ
el.style.setProperty('--tx', 0);
el.style.setProperty('--ty', 0);
el.style.setProperty('--sx', 1);
el.style.setProperty('--sy', 1);
}
button.onclick = () =>
animateLayout(box, () => stage.toggleAttribute('data-last'));注意点・対応状況
@propertyと、登録したカスタムプロパティの transition は主要ブラウザで使える(Baseline)。- 登録カスタムプロパティのアニメーションはコンポジタに渡されず、メインスレッドで毎フレームスタイル再計算が走る。
--sx/--syは継承するので子孫も再計算の対象になる。カード1枚・ダイアログ・短いリスト程度向き。数百要素や重いページではelement.animate()でtransformを直接動かす方が安全。 - 子の逆スケール補正は「子が親の padding の位置に並んでいる」前提。複雑なレイアウトでは追加の計算が必要。
prefers-reduced-motion: reduceのときは transition を外して即座に移動させる。- ページ全体の遷移なら View Transitions も候補(ガイド: CSSだけで作るアニメーション 参照)。
出典
- @argyleink(2026-11-10、解説記事 nerdy.dev「PropFLIP」)