コンテンツにスキップ

Secondary Action(副次的な動作)

主動作に添える小さな動きが、動作に意図や感情を加えます。secondaryAction は 4 種類のプリセット(wiggle / pulse / float / sway)を返し、accompany はそれを主動作の尺の中に収まるよう遅らせ、弱めて配置します。

LIVE
通知 3件
「jump + accompany("wiggle")」を押すと、通知カードが跳ね(主動作)、少し遅れて中のアイコンが揺れる(副次動作)。個性を切り替えると跳ね方と揺れの尺が一緒に変わる。

Secondary action は、主動作を補強するために同時に起こる副次的な動きです。歩きながら腕を振る、怒って歩きながら拳を握る、口笛を吹きながら歩く、といった動きは、主動作(歩く)に性格や感情を加えます。

重要なのは主従関係です。副次動作は主動作を支えるものであり、観客の注意を奪ってはいけません。表情の変化のような繊細な副次動作は、大きな主動作の最中だと見落とされるため、主動作の直前や直後に置くのが定石です。副次動作が目立ちすぎるなら、それはもう副次ではなく、演出を組み直すべきサインです。

follow through(慣性で遅れて動く部分)と似ていますが、follow through が物理的な結果として「起きてしまう」動きなのに対し、secondary action はキャラクターが「する」動きで、意図や感情を伝えるために加えるものです。

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 の既定 attenuation 0.6 では、wiggle は最大 6 度、pulse は 4.8% の拡大です。
  • 少し遅れて始める: 主動作と同時に始まると一つの動きに溶けてしまいます。accompany の既定 lag 0.15 では、450ms の jump に対して 68ms 遅れて始まり、383ms で主動作と同時に終わります。
  • ループは控えめに: floatswayiterations: Infinity で回すと、画面に常に動くものが残ります。業務 UI では一回限りにし、ループは空状態やオンボーディングなど、他に読むものが少ない画面に限ります。自動で始まり 5 秒以上続く動きには、WCAG 2.2.2 により停止手段が必要です。
type SecondaryKind = "wiggle" | "pulse" | "float" | "sway";
function secondaryAction(kind: SecondaryKind, options?: SecondaryOptions): MotionSpec;
オプション 既定値 説明
amplitude number 1 プリセットに対する強さの倍率。副次動作が主役を食わないよう 1 未満を推奨
duration number 500durations.deliberate 1 イテレーションの長さ(ms)
delay number なし 開始までの遅延(ms)
iterations number なし(1 回) 繰り返し回数。Infinity で無限ループ

amplitude はプリセットの角度・倍率・距離に掛かります。たとえば swayamplitude: 0.5 にすると ±1.5 度です。1 を超える値も受け付けます(デモは accompany 経由で 1.4 を使い、wiggle を最大 14 度にしています)。

返り値は普通の MotionSpec で、origin(回転の中心)は設定しません。回転は要素の中心で起こるので、ベルのように上端を支点に揺らしたいときは origin を上書きします。

const ring = { ...secondaryAction("wiggle"), origin: "50% 0%" };
function accompany(primary: MotionSpec, kind: SecondaryKind, options?: AccompanyOptions): MotionSpec;
オプション 既定値 説明
attenuation number 0.6 プリセットに対する強さ。secondaryActionamplitude になる
lag number 0.15 主動作の尺のこの割合だけ遅れて始まる。反応として読ませるための遅れ

accompany は次の値で secondaryAction(kind, ...) を呼びます。

  • amplitude = attenuation
  • duration = round(primary.duration × (1 − lag))
  • delay = (primary.delay ?? 0) + round(primary.duration × lag)

結果として、副次動作は主動作の最初のイテレーションと同時に終わります。primaryiterationsdirection は引き継ぎません。個性の 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)を休止姿勢へ戻す
}
  • 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 に追従します。