コンテンツにスキップ

既定値に組み込まれた UI ガイドライン

twelve-principles は、UI モーションの定石をドキュメントで勧めるだけでなく、定数と既定値として埋め込んでいます。何も指定しなければガイドラインどおりに動き、外れたいときは明示的にオプションで上書きする、という設計です。このページでは各ガイドラインと、それを実装している定数・API を対応づけます。外れ方は ガードレールを意図して外す にまとめています。

ガイドライン 既定値・定数 実装している API
ユーザー起点の動きは 300ms 以内 USER_INITIATED_MAX_MS = 300durations レシピ、振る舞い、travelDurationposeToPose
スタッガーは 1 項目 50ms 以内 MAX_STAGGER_MS = 50cap の既定値)、each 30、total 300 staggerDelaysoverlapuseCascade
入場は ease-out、退場は ease-in easings.out / easings.in enterexithoverable
画面内の移動は ease-in-out easings.inOut poseToPoseanimateToarc
linear は進捗表示だけ easings.linear 空間的な API の既定値には使われない
押下は scale 0.95〜0.98 pressDepth が 0.02〜0.05 にクランプ(guardrails: true のとき) pressableusePress
オーバーシュートはスプリングで spring()settleSpring followThroughenter("pop")、解除の戻り
焦点は 1 つ stagingPosesamplitude / attenuation stagesecondaryActionaccompany
コンポジタで処理できるプロパティを使う transformopacity が中心 poseToStyle

クリックやキー入力への応答は、300ms を超えると「待たされている」と感じられます。USER_INITIATED_MAX_MS はこの上限を表す定数で、travelDurationmax の既定値になっています。尺のトークンは次の 5 段階です。

durations ms 主な用途
instant 100 押下の沈み込み
fast 150 退場、チルトの追従
base 200 入場、ホバーのリフト、animateTo の既定
slow 300 enter("pop")、ホバー解除、ステージング開始、poseToPose の既定
deliberate 500 システム起点の演出(オンボーディング、空状態)。secondaryAction の既定

ライブラリ内部で使われている尺は次のとおりです(naturaltempo 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 区間ごと

jumpshakesecondaryAction は注意を引くための演出なので、300ms の上限の外に置かれています。また、Personality の tempo はすべての尺に掛かるため、calm(1.25)では入場が 250ms、pop が 375ms になり、上限を超えます。落ち着いた性格を選ぶこと自体が、応答性とのトレードオフです。

遠くへ動くものは長く、近くへ動くものは短くするのが自然ですが、距離に比例させると長距離が間延びします。travelDuration(distancePx, { min = 150, max = 300, reference = 800 }) は距離の平方根で補間します。

距離 100px 400px 800px 以上
travelDuration 203ms 256ms 300ms

arc()duration を省略するとこの値を使います。

リストの項目を少しずつずらして入場させると、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 にクランプされます。

overlapuseCascade はこの関数で遅延を計算し、cap もそのまま受け取ります。

動き イージング
入場 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)で、enterbase(200ms)より短く設定されています。消えていくものをじっくり見せる必要はありません。
  • 画面内の移動は ease-in-out。 poseToPose の区間、animateToarc の既定値です。
  • linear は空間的な動きに使わない。 一定速度の移動は機械的に見えます。ライブラリの空間的な API の既定値に linear は使われていません。

押したボタンが縮む量は、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() を持つブラウザならどこでも同じ曲線になります。

同時に動くものが増えるほど、どこを見ればよいかが分からなくなります(Staging)。

  • stage(focus) / useStage(active) は主役を前に出し(scale 1.02、elevation 12)、周囲を下げます(opacity 0.5、blur 2px、scale 0.98)。同時にステージングする要素は 1 つにしてください。
  • secondaryActionamplitude は 1 未満を推奨しています。主動作より目立つ副次動作は、主動作を食ってしまいます。
  • accompany(primary, kind) は既定で強さを 0.6 倍(attenuation)にし、主動作の 15%(lag)遅れて始めるので、「主動作への反応」として読めます。

描画コスト: transform と opacity を中心に

Section titled “描画コスト: transform と opacity を中心に”

ブラウザは transformopacity の変化を、レイアウトやペイントをやり直さずにコンポジタだけで処理できます。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 のようなレイアウトプロパティがないので、レイアウトを発生させるアニメーションはそもそも書けません。

注意が必要なのは blurelevation です。どちらもペイントプロパティで、アニメーション中は毎フレーム再描画が走ります。

  • stage は周囲の全要素に blur を掛けます。 既定では主役の兄弟要素すべてが対象です。兄弟が数十個あるグリッドでは、stage(focus, { blur: 0 }) にするか、surroundings で対象を絞ってください。
  • elevation は同時に動かす数を抑えます。 hoverablelift の影は、1 度に 1 要素が動く分には問題になりません。リストの全項目の elevation を一斉にアニメーションさせるのは避けます。
  • 影は光源を 1 つに揃えるための表現です。 shadowForElevation は elevation が高いほど影を遠く・大きく・薄くします。値を直接 box-shadow に書くより、elevation で指定するほうが、ほかの要素と一貫した光源になります。

上のガイドラインは既定値であって禁止ではありません。祝福・オンボーディング・ゲームなど、動きそのものを見せたい場面では、次の 3 つを明示的に選ぶことで外に出られます。どれも既定では無効です。

外したいもの 方法 既定
押下の深さ(最大 5%) guardrails: false の Personality(bouncy / cartoon、または definePersonality({ guardrails: false }))。上限が 20% になる guardrails: true
スタッガーの間隔(最大 50ms) staggerDelays / overlap / useCascadecap を渡す。必要なら total も上げる cap = MAX_STAGGER_MS(50)
尺(300ms 以内)と振れ幅 派手なレシピ(rubberBand / tada / bounceIn など。800〜1300ms)、tempoexaggeration の大きい 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 で限定し、よく繰り返す操作(ホバー、フォーム送信、画面遷移)には持ち込まないでください。詳しくは 派手な動き を参照してください。

新しいモーションを足すときは、次を確認してください。

  • ユーザーの操作への応答なら、尺は durations のトークン(300ms 以内)から選んだか。
  • 入場は easings.out、退場は easings.in で、退場のほうが短いか。
  • 行き過ぎが必要なら、ベジェではなく spring() / followThrough を使ったか。
  • 同時に目立つ動きは 1 つだけか。
  • blurelevation を多数の要素で同時にアニメーションさせていないか。
  • prefers-reduced-motion のときの見え方を確認したか(アクセシビリティ)。
  • ガードレールを外した(guardrails: falsecap、派手なレシピ)なら、それは一度きりの場面に限られ、範囲が MotionProvider などで限定されているか。

関連: TimingSlow In & Slow OutStagingAppeal派手な動き