Secondary Action(副次的な動作)
主動作に添える小さな動きが、動作に意図や感情を加えます。secondaryAction は 4 種類のプリセット(wiggle / pulse / float / sway)を返し、accompany はそれを主動作の尺の中に収まるよう遅らせ、弱めて配置します。
アニメーションにおける原則
Section titled “アニメーションにおける原則”Secondary action は、主動作を補強するために同時に起こる副次的な動きです。歩きながら腕を振る、怒って歩きながら拳を握る、口笛を吹きながら歩く、といった動きは、主動作(歩く)に性格や感情を加えます。
重要なのは主従関係です。副次動作は主動作を支えるものであり、観客の注意を奪ってはいけません。表情の変化のような繊細な副次動作は、大きな主動作の最中だと見落とされるため、主動作の直前や直後に置くのが定石です。副次動作が目立ちすぎるなら、それはもう副次ではなく、演出を組み直すべきサインです。
follow through(慣性で遅れて動く部分)と似ていますが、follow through が物理的な結果として「起きてしまう」動きなのに対し、secondary action はキャラクターが「する」動きで、意図や感情を伝えるために加えるものです。
UI への翻訳
Section titled “UI への翻訳”UI の主動作は、ボタンの押下、カードの入場、通知の到着といった要素全体の動きです。副次動作はその内側の小さな部品、つまりアイコン、バッジ、イラストの動きとして現れます。
| プリセット | 動き(amplitude = 1) |
使いどころ |
|---|---|---|
wiggle |
rotate が 0 → -10 → 8 → -5 → 2 → 0 度と減衰 |
ベルやアイコンがタップや通知に反応する |
pulse |
scale が 1 → 1.08 → 1 |
バッジの件数更新、保存完了の合図 |
float |
y が 0 → -4 → 0 px |
空状態のイラスト、待機中の要素 |
sway |
rotate が 0 → 3 → -3 → 0 度 |
吊り下げたタグ、ゆるい揺れ |
すべての区間に easings.inOut が付き、既定の尺は durations.deliberate(500ms)です。
- 強さは主動作の半分程度:
accompanyの既定attenuation0.6 では、wiggleは最大 6 度、pulseは 4.8% の拡大です。 - 少し遅れて始める: 主動作と同時に始まると一つの動きに溶けてしまいます。
accompanyの既定lag0.15 では、450ms のjumpに対して 68ms 遅れて始まり、383ms で主動作と同時に終わります。 - ループは控えめに:
floatやswayをiterations: Infinityで回すと、画面に常に動くものが残ります。業務 UI では一回限りにし、ループは空状態やオンボーディングなど、他に読むものが少ない画面に限ります。自動で始まり 5 秒以上続く動きには、WCAG 2.2.2 により停止手段が必要です。
secondaryAction
Section titled “secondaryAction”type SecondaryKind = "wiggle" | "pulse" | "float" | "sway";
function secondaryAction(kind: SecondaryKind, options?: SecondaryOptions): MotionSpec;| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
amplitude |
number |
1 |
プリセットに対する強さの倍率。副次動作が主役を食わないよう 1 未満を推奨 |
duration |
number |
500(durations.deliberate) |
1 イテレーションの長さ(ms) |
delay |
number |
なし | 開始までの遅延(ms) |
iterations |
number |
なし(1 回) | 繰り返し回数。Infinity で無限ループ |
amplitude はプリセットの角度・倍率・距離に掛かります。たとえば sway を amplitude: 0.5 にすると ±1.5 度です。1 を超える値も受け付けます(デモは accompany 経由で 1.4 を使い、wiggle を最大 14 度にしています)。
返り値は普通の MotionSpec で、origin(回転の中心)は設定しません。回転は要素の中心で起こるので、ベルのように上端を支点に揺らしたいときは origin を上書きします。
const ring = { ...secondaryAction("wiggle"), origin: "50% 0%" };accompany
Section titled “accompany”function accompany(primary: MotionSpec, kind: SecondaryKind, options?: AccompanyOptions): MotionSpec;| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
attenuation |
number |
0.6 |
プリセットに対する強さ。secondaryAction の amplitude になる |
lag |
number |
0.15 |
主動作の尺のこの割合だけ遅れて始まる。反応として読ませるための遅れ |
accompany は次の値で secondaryAction(kind, ...) を呼びます。
amplitude=attenuationduration=round(primary.duration × (1 − lag))delay=(primary.delay ?? 0) + round(primary.duration × lag)
結果として、副次動作は主動作の最初のイテレーションと同時に終わります。primary の iterations や direction は引き継ぎません。個性の tempo で主動作の尺が変われば、副次動作の尺も自動的に追従します。
import { accompany, animateTo, jump, play, secondaryAction } from "twelve-principles";
// 通知: カード全体が跳ね(主動作)、中のベルが遅れて揺れる(副次動作)const card = document.querySelector<HTMLElement>(".notification")!;const bell = card.querySelector<HTMLElement>(".notification-bell")!;
export function notify() { const primary = jump({ height: 10 }); play(card, primary); play(bell, { ...accompany(primary, "wiggle"), origin: "50% 0%" });}
// バッジ: 件数が変わったら一度だけ脈打つconst badge = document.querySelector<HTMLElement>(".badge")!;export function setCount(n: number) { badge.textContent = String(n); play(badge, secondaryAction("pulse", { amplitude: 0.75, duration: 300 }));}
// 空状態のイラスト: ゆっくり浮遊し続け、データが来たら休止姿勢へ戻すconst art = document.querySelector<HTMLElement>(".empty-art")!;play(art, secondaryAction("float", { amplitude: 0.75, duration: 2400, iterations: Infinity }));
export function onDataLoaded() { animateTo(art, {}); // 使っていたキー(y)を休止姿勢へ戻す}import { useEffect } from "react";import { accompany, jump, secondaryAction } from "twelve-principles";import { useMotion, useMotionConfig } from "twelve-principles/react";
export function NotificationCard({ count }: { count: number }) { const { personality } = useMotionConfig(); const card = useMotion<HTMLDivElement>(); const bell = useMotion<HTMLSpanElement>();
const ring = () => { const primary = jump({ personality, height: 10 }); card.play(primary); bell.play({ ...accompany(primary, "wiggle"), origin: "50% 0%" }); };
return ( <div ref={card.ref} className="notification"> <span ref={bell.ref} className="notification-bell" aria-hidden="true" /> <span>通知 {count}件</span> <button onClick={ring}>鳴らす</button> </div> );}
export function Badge({ count }: { count: number }) { const badge = useMotion<HTMLSpanElement>(); useEffect(() => { if (count > 0) badge.play(secondaryAction("pulse", { amplitude: 0.75, duration: 300 })); }, [count]); return ( <span ref={badge.ref} className="badge"> {count} </span> );}ガイドラインと落とし穴
Section titled “ガイドラインと落とし穴”- Squash & Stretch / Anticipation: デモの主動作
jumpは溜め・伸び・着地の潰れを含むレシピです。副次動作はその尺にaccompanyで合わせます。 - Follow Through & Overlapping:
wiggleの減衰は余韻として働きます。lagによる遅れは overlapping action と同じ「部位ごとの時間差」です。 - Timing: 既定の尺 500ms は
durations.deliberateで、ユーザー操作への応答(300ms 以内)ではなく演出用の長さです。 - Solid Drawing:
solid({ ...secondaryAction("sway", { amplitude: 0.5 }), iterations: Infinity })のように、揺れを 3D の傾きとして見せられます。 - Appeal: 主動作を個性から作れば、
accompanyの尺も個性のtempoに追従します。