既定値に組み込まれた UI ガイドライン
twelve-principles は、UI モーションの定石をドキュメントで勧めるだけでなく、定数と既定値として埋め込んでいます。何も指定しなければガイドラインどおりに動き、外れたいときは明示的にオプションで上書きする、という設計です。このページでは各ガイドラインと、それを実装している定数・API を対応づけます。外れ方は ガードレールを意図して外す にまとめています。
| ガイドライン | 既定値・定数 | 実装している API |
|---|---|---|
| ユーザー起点の動きは 300ms 以内 | USER_INITIATED_MAX_MS = 300、durations |
レシピ、振る舞い、travelDuration、poseToPose |
| スタッガーは 1 項目 50ms 以内 | MAX_STAGGER_MS = 50(cap の既定値)、each 30、total 300 |
staggerDelays、overlap、useCascade |
| 入場は ease-out、退場は ease-in | easings.out / easings.in |
enter、exit、hoverable |
| 画面内の移動は ease-in-out | easings.inOut |
poseToPose、animateTo、arc |
| linear は進捗表示だけ | easings.linear |
空間的な API の既定値には使われない |
| 押下は scale 0.95〜0.98 | pressDepth が 0.02〜0.05 にクランプ(guardrails: true のとき) |
pressable、usePress |
| オーバーシュートはスプリングで | spring()、settleSpring |
followThrough、enter("pop")、解除の戻り |
| 焦点は 1 つ | stagingPoses、amplitude / attenuation |
stage、secondaryAction、accompany |
| コンポジタで処理できるプロパティを使う | transform と opacity が中心 |
poseToStyle |
尺: ユーザー起点は 300ms 以内
Section titled “尺: ユーザー起点は 300ms 以内”クリックやキー入力への応答は、300ms を超えると「待たされている」と感じられます。USER_INITIATED_MAX_MS はこの上限を表す定数で、travelDuration の max の既定値になっています。尺のトークンは次の 5 段階です。
durations |
ms | 主な用途 |
|---|---|---|
instant |
100 | 押下の沈み込み |
fast |
150 | 退場、チルトの追従 |
base |
200 | 入場、ホバーのリフト、animateTo の既定 |
slow |
300 | enter("pop")、ホバー解除、ステージング開始、poseToPose の既定 |
deliberate |
500 | システム起点の演出(オンボーディング、空状態)。secondaryAction の既定 |
ライブラリ内部で使われている尺は次のとおりです(natural、tempo 1 のとき)。
| 場面 | 尺 | イージング |
|---|---|---|
押下で沈む(pressable) |
100ms | easings.out |
| 押下・チルトの解除 | settleSpring(静定まで 475ms) |
スプリング |
ホバーで浮く / 戻る(hoverable) |
200ms / 300ms | easings.out / easings.inOut |
チルトの追従(tiltable) |
150ms | easings.out |
ステージング開始 / 解除(stage) |
300ms / 200ms | easings.out / easings.inOut |
入場 / pop 入場 / 退場 |
200ms / 300ms / 150ms | easings.out / スプリング / easings.in |
usePresence の退場中の反転 |
200ms | easings.out |
jump / shake |
450ms / 400ms | 区間ごと |
jump、shake、secondaryAction は注意を引くための演出なので、300ms の上限の外に置かれています。また、Personality の tempo はすべての尺に掛かるため、calm(1.25)では入場が 250ms、pop が 375ms になり、上限を超えます。落ち着いた性格を選ぶこと自体が、応答性とのトレードオフです。
距離に応じた尺
Section titled “距離に応じた尺”遠くへ動くものは長く、近くへ動くものは短くするのが自然ですが、距離に比例させると長距離が間延びします。travelDuration(distancePx, { min = 150, max = 300, reference = 800 }) は距離の平方根で補間します。
| 距離 | 100px | 400px | 800px 以上 |
|---|---|---|---|
travelDuration |
203ms | 256ms | 300ms |
arc() は duration を省略するとこの値を使います。
スタッガー: 1 項目 50ms 以内
Section titled “スタッガー: 1 項目 50ms 以内”リストの項目を少しずつずらして入場させると、1 つのまとまった動きとして読めます(Overlapping action)。ただし 1 項目あたりの遅延が 50ms を超えると、カスケードではなく「遅いリスト」に見えます。
staggerDelays(count, { each = 30, cap = MAX_STAGGER_MS, total = 300, from = "first" }) は、ステップを Math.min(each, cap, total / 最大距離) に制限します。cap の既定値 MAX_STAGGER_MS(50)がこのガイドラインです。
- 5 項目なら
[0, 30, 60, 90, 120]。 - 20 項目なら 1 ステップ約 15.8ms に縮み、最後の項目は 300ms で始まります。項目数が増えても全体は
totalに収まります。 each: 80を指定しても、capを指定しなければ 50ms にクランプされます。
overlap と useCascade はこの関数で遅延を計算し、cap もそのまま受け取ります。
イージングの向き
Section titled “イージングの向き”| 動き | イージング | 値 |
|---|---|---|
| 入場 | easings.out |
cubic-bezier(0.22, 1, 0.36, 1) |
| 退場 | easings.in |
cubic-bezier(0.55, 0, 1, 0.45) |
| 画面内の 2 状態間の移動 | easings.inOut |
cubic-bezier(0.65, 0, 0.35, 1) |
| 進捗・スピナー・スクラブ | easings.linear |
linear |
- 入場は ease-out。 画面に入ってくるものは、すでに動いている状態で現れて減速しながら止まります。最初の数フレームで大きく動くので、反応が速く感じられます。
- 退場は ease-in、しかも入場より短い。 去るものは加速しながら消えます。
exitの既定はfast(150ms)で、enterのbase(200ms)より短く設定されています。消えていくものをじっくり見せる必要はありません。 - 画面内の移動は ease-in-out。
poseToPoseの区間、animateTo、arcの既定値です。 - linear は空間的な動きに使わない。 一定速度の移動は機械的に見えます。ライブラリの空間的な API の既定値に
linearは使われていません。
押下: scale 0.95〜0.98
Section titled “押下: scale 0.95〜0.98”押したボタンが縮む量は、2〜5% が「押した手応え」と「形が崩れない」ことの両立点です。pressDepth(personality) は personality.squash × 0.75 を、guardrails: true の Personality では 0.02〜0.05 にクランプします。組み込みの natural / snappy / calm / playful と、それらを base にして definePersonality で作った Personality(guardrails を上書きしない限り)では、押下中の scale は 0.95〜0.98 の範囲に収まります。
| Personality | 押下中の scale |
|---|---|
natural |
0.97 |
snappy |
0.9775 |
calm |
0.98 |
playful |
0.95 |
guardrails: false の Personality(bouncy / cartoon)では上限が 0.2 に広がります(bouncy 0.91、cartoon 0.835)。pressable(el, { depth }) で明示的に指定した場合はどちらでもクランプされません。
オーバーシュートはスプリングで
Section titled “オーバーシュートはスプリングで”行き過ぎて戻る動き(Follow through)は、cubic-bezier の y を 1 より大きくすることでも近似できます(easings.overshoot)。しかしベジェは 1 回しか行き過ぎず、戻りの減衰を表現できません。ライブラリは行き過ぎが必要な場面では減衰振動の spring() を使います。
enter("pop"):followThroughで最終区間をスプリングにする(bounceは最低 0.25)。- 押下・チルトの解除:
settleSpring(personality)。 followThrough(spec, { bounce = 0.3 })とoverlapの後続パート。
スプリングは JS 関数なので、compile の時点で 60fps 相当の線形キーフレーム(12〜120 枚)に焼き込まれます。CSS の linear() 関数には依存しないため、WAAPI と commitStyles() を持つブラウザならどこでも同じ曲線になります。
焦点は 1 つ
Section titled “焦点は 1 つ”同時に動くものが増えるほど、どこを見ればよいかが分からなくなります(Staging)。
stage(focus)/useStage(active)は主役を前に出し(scale1.02、elevation12)、周囲を下げます(opacity 0.5、blur 2px、scale 0.98)。同時にステージングする要素は 1 つにしてください。secondaryActionのamplitudeは 1 未満を推奨しています。主動作より目立つ副次動作は、主動作を食ってしまいます。accompany(primary, kind)は既定で強さを 0.6 倍(attenuation)にし、主動作の 15%(lag)遅れて始めるので、「主動作への反応」として読めます。
描画コスト: transform と opacity を中心に
Section titled “描画コスト: transform と opacity を中心に”ブラウザは transform と opacity の変化を、レイアウトやペイントをやり直さずにコンポジタだけで処理できます。Pose のキーのほとんどはこの 2 つに変換されます。
Pose のキー |
CSS | 描画コスト |
|---|---|---|
x / y / z / scale / scaleX / scaleY / rotate / rotateX / rotateY / skewX / skewY |
transform |
コンポジタのみ |
opacity |
opacity |
コンポジタのみ |
blur |
filter: blur() |
毎フレームのペイント |
elevation |
box-shadow(2 層) |
毎フレームのペイント |
Pose には width / height / top / left のようなレイアウトプロパティがないので、レイアウトを発生させるアニメーションはそもそも書けません。
注意が必要なのは blur と elevation です。どちらもペイントプロパティで、アニメーション中は毎フレーム再描画が走ります。
stageは周囲の全要素に blur を掛けます。 既定では主役の兄弟要素すべてが対象です。兄弟が数十個あるグリッドでは、stage(focus, { blur: 0 })にするか、surroundingsで対象を絞ってください。elevationは同時に動かす数を抑えます。hoverableやliftの影は、1 度に 1 要素が動く分には問題になりません。リストの全項目のelevationを一斉にアニメーションさせるのは避けます。- 影は光源を 1 つに揃えるための表現です。
shadowForElevationは elevation が高いほど影を遠く・大きく・薄くします。値を直接box-shadowに書くより、elevationで指定するほうが、ほかの要素と一貫した光源になります。
ガードレールを意図して外す
Section titled “ガードレールを意図して外す”上のガイドラインは既定値であって禁止ではありません。祝福・オンボーディング・ゲームなど、動きそのものを見せたい場面では、次の 3 つを明示的に選ぶことで外に出られます。どれも既定では無効です。
| 外したいもの | 方法 | 既定 |
|---|---|---|
| 押下の深さ(最大 5%) | guardrails: false の Personality(bouncy / cartoon、または definePersonality({ guardrails: false }))。上限が 20% になる |
guardrails: true |
| スタッガーの間隔(最大 50ms) | staggerDelays / overlap / useCascade に cap を渡す。必要なら total も上げる |
cap = MAX_STAGGER_MS(50) |
| 尺(300ms 以内)と振れ幅 | 派手なレシピ(rubberBand / tada / bounceIn など。800〜1300ms)、tempo や exaggeration の大きい Personality |
ガイドライン内のレシピ |
import { MotionProvider } from "twelve-principles/react";import type { ReactNode } from "react";
// アプリ全体はガードレールあり。祝福画面だけ外すexport function CelebrationScope({ children }: { children: ReactNode }) { return <MotionProvider personality="cartoon">{children}</MotionProvider>;}範囲は入れ子の MotionProvider で限定し、よく繰り返す操作(ホバー、フォーム送信、画面遷移)には持ち込まないでください。詳しくは 派手な動き を参照してください。
チェックリスト
Section titled “チェックリスト”新しいモーションを足すときは、次を確認してください。
- ユーザーの操作への応答なら、尺は
durationsのトークン(300ms 以内)から選んだか。 - 入場は
easings.out、退場はeasings.inで、退場のほうが短いか。 - 行き過ぎが必要なら、ベジェではなく
spring()/followThroughを使ったか。 - 同時に目立つ動きは 1 つだけか。
blurやelevationを多数の要素で同時にアニメーションさせていないか。prefers-reduced-motionのときの見え方を確認したか(アクセシビリティ)。- ガードレールを外した(
guardrails: false、cap、派手なレシピ)なら、それは一度きりの場面に限られ、範囲がMotionProviderなどで限定されているか。