npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

twelve-principles

v0.1.1

Published

Disney's 12 principles of animation as composable TypeScript primitives for UI components. Web Animations API based, zero runtime dependencies, optional React bindings.

Readme

twelve-principles

ディズニーの「アニメーションの12原則」を、UI コンポーネント向けの合成可能なプリミティブとして実装した TypeScript ライブラリです。各原則を「ポーズを返す関数」「モーションを変換する関数」「要素に振る舞いを付ける関数」に落とし込み、Web Animations API (WAAPI) で再生します。ランタイム依存はゼロ、React バインディングは twelve-principles/react から任意で使えます(react >= 18 は optional peer dependency)。

npm i twelve-principles

解説サイト(ライブデモ付き): https://twelve-principles.iru-yo.com

目次

クイックスタート

Vanilla

import { enter, play, pressable } from "twelve-principles";

const card = document.querySelector<HTMLElement>(".card")!;
play(card, enter("rise")); // 下からフェードイン(ease-out)

const button = document.querySelector<HTMLButtonElement>("button")!;
const cleanup = pressable(button); // 押下で縮み、離すとスプリングで戻る
// cleanup(); // リスナーを外し、静止状態に戻す

React

import { jump } from "twelve-principles";
import { Motion, MotionProvider, Presence, useMotion } from "twelve-principles/react";

function Mascot() {
  const { ref, play } = useMotion<HTMLDivElement>();
  return <div ref={ref} onClick={() => play((p) => jump({ personality: p }))}>!</div>;
}

export function App({ open }: { open: boolean }) {
  return (
    <MotionProvider personality="playful">
      <Motion as="button" press hover enter="pop">
        Save
      </Motion>
      <Presence show={open} enter="pop" exit="pop">
        Dialog
      </Presence>
      <Mascot />
    </MotionProvider>
  );
}
  • MotionProvider — personality(既定 "natural")と reducedMotion(既定 "auto")をサブツリー全体に配る。
  • Motion — as(既定 "div")、press / hover / tilt(boolean またはオプション、既定 false)、enter(レシピ名・spec・(personality) => spec、既定 false)。
  • Presence — show が false になっても退場モーションが終わるまでアンマウントしない。enter 既定 "rise"、exit 既定 "fade"、initial 既定 true(初回表示時にも入場を再生)。
  • useMotion() — { ref, play, animateTo, stop }。play は spec か (personality) => spec を受け取る。

同等のフック: usePress / useHover / useTilt(引数 false で無効化)、useEnter(input = "rise")、usePresence(show, { enter, exit, initial }) → { present, ref }、useStage(active, options)、useCascade(input = "rise", options)、useMotionConfig()。

メンタルモデル

Pose  →  PoseFrame  →  MotionSpec  →  compile(spec) → WAAPI keyframes/options
                                   └→  play(el, spec) → Animation
  • Pose — 要素の見た目の状態。x / y / z(px)、scale / scaleX / scaleY、rotate / rotateX / rotateY / skewX / skewY(deg)、opacity、blur(px)、elevation(0 = 面に接地、~24 = 高く浮く)。
  • PoseFrame — Pose に offset(0..1、省略時は WAAPI と同じく均等配置)と easing(そのフレームから始まる区間に適用)を加えたもの。
  • MotionSpec — frames, duration(ms), delay, easing(イテレーション全体), iterations, direction, origin(transform-origin), perspective(px)。
  • compile(spec) — { keyframes, options } を返す純関数。play(el, spec, options) がそれを element.animate() で再生し Animation を返す。

設計上の約束:

  1. ポーズは静止状態に対する絶対値。 { x: 10 } は「今の位置から +10px」ではなく「無変形の状態から 10px」。省略したキーは静止値(REST: x: 0, scale: 1, opacity: 1 …)として扱われる。
  2. アニメーション対象の要素では、ライブラリが transform / transform-origin / opacity / filter / box-shadow を所有する。 自前の CSS transform が必要なら、ラッパー要素に分けること。
  3. 1要素につき1モーション。 新しい play は再生中のモーションを中断して置き換える。途切れなく繋ぎたいときは currentPose(el)(再生中ならその瞬間のポーズ、終了後なら最後の静止ポーズ)を起点にするか、それを内部で行う animateTo(el, pose, options) を使う。
  4. 静止状態で終わるモーションはインラインスタイルを消す。 終端ポーズが REST と等しければ上記の所有プロパティをインラインから除去し、スタイルシートに制御を返す。それ以外は commitStyles() で終端をインラインに固定してアニメーションを解放する(play の persist: false なら終了後に元へ戻る)。stop(el) はその場で止める。
  5. レイヤーで合成する。 setLayer(el, name, pose | null, options) は名前付きレイヤーを設定・解除し、全レイヤーの合成ポーズへ遷移する(平行移動・回転・elevation は加算、scale・opacity は乗算 — composePoses)。pressable / hoverable / tiltable / stage はそれぞれ "press" / "hover" / "tilt" / "stage" レイヤーを使うので、同じ要素に重ねても互いを上書きしない。
import { animateTo, currentPose, poseToPose, play, setLayer, spring } from "twelve-principles";

animateTo(el, { x: 120 }, { transition: spring({ bounce: 0.3 }) }); // 現在位置から中断なしに遷移
play(el, poseToPose([currentPose(el), { x: 0 }]));                   // 同じことを明示的に
setLayer(el, "drag", { rotate: 4 });                                  // 他のレイヤーと合成される

12原則と API の対応

| # | 原則 | API | | --- | --- | --- | | 1 | Squash & stretch | deform, squash, stretch, squashStretch | | 2 | Anticipation | anticipate, windUp | | 3 | Staging | stagingPoses, stage, useStage | | 4 | Straight ahead & pose to pose | straightAhead, poseToPose | | 5 | Follow through & overlapping action | followThrough, overlap, spring, useCascade | | 6 | Slow in & slow out | easings, cubicBezier | | 7 | Arcs | arc, arcPoint | | 8 | Secondary action | secondaryAction, accompany | | 9 | Timing | durations, duration, travelDuration, staggerDelays, staggerDistances | | 10 | Exaggeration | exaggerate | | 11 | Solid drawing | lift, tiltToward, solid, shadowForElevation, tiltable | | 12 | Appeal | personalities, definePersonality, MotionProvider |

1. Squash & stretch

物体は衝撃で潰れ、加速すると伸びる。体積を保ったまま変形させることで、重さと柔らかさが伝わる。UI では着地・押下・通知バッジの弾みなど、ごく小さな変形(3〜5%)として使う。

  • deform(amount, axis = "y") — scaleX × scaleY = 1 を保つ変形ポーズ。amount > 0 で伸び、< 0 で潰れ(> -1 が必要)。
  • squash(amount = 0.05, axis = "y") / stretch(amount = 0.05, axis = "y")
  • squashStretch({ intensity = 0.05, axis = "y", duration = 300, origin }) — 接地 → 潰れ → 反発で伸び → 小さく潰れ → 静止。origin の既定は axis が "y" なら "50% 100%"、"x" なら "0% 50%"。
play(badge, squashStretch({ intensity: 0.08 }));

2. Anticipation

動作の前に逆方向の小さな「溜め」を入れ、これから何が起きるかを観客に予告する(ジャンプ前にしゃがむ、投げる前に腕を引く)。UI では削除・送信など大きな動きの直前に短い逆方向の動きを置く。

  • anticipate(spec, { amount = 0.15, share = 0.3, fit = "compress" }) — 最初の区間の前に溜めフレームを挿入。share はタイムライン中の溜めの割合(0〜1 の開区間)。"compress" は総尺を維持(ユーザー起点の時間予算を守る)、"extend" は元の動作の尺を維持して全体を延ばす。
  • windUp(from, to, amount = 0.15) — from → to に対する溜めポーズ(空間的なキーのみ。opacity は引き戻さない)。
play(card, anticipate(poseToPose([{ x: 0 }, { x: 240, opacity: 0 }]), { amount: 0.2 }));

3. Staging

見せたいものを一つに絞り、構図・光・動きで視線を誘導する。UI では選択中のカードを前へ出し、周囲を暗く・ぼかし・後退させる。

  • stagingPoses({ dim = 0.5, blur = 2, recede = 0.02, lift = 12 }) → { focus, surroundings }(ポーズのみ)。
  • stage(focus, options) → { release(): Promise<void> }。surroundings の既定は focus の兄弟要素。personality / reducedMotion も受け取る。適用中は focus に z-index: 10(position: static なら relative)を付け、release() 完了時に戻す。
  • useStage(active, options) — active の間だけステージングする React フック。surroundings はステージング開始時に読まれる(インライン配列でも毎レンダーで再ステージしない)。
const handle = stage(selectedCard, { dim: 0.6 });
await handle.release();

4. Straight ahead & pose to pose

ストレートアヘッドは最初から順に1コマずつ描く手法、ポーズ・トゥ・ポーズは要所のキーポーズを先に決めて中割りを埋める手法。UI では通常ポーズ・トゥ・ポーズ(中割りはブラウザが担当)を使い、物理シミュレーションなどキーでは表せない動きだけを時間関数から直接サンプリングする。

  • poseToPose(keys, { duration = 300, delay, easing, iterations, direction, origin, perspective }) — キーは2つ以上。easing のない区間は easings.inOut になる。
  • straightAhead(draw, { duration, fps = 60, ... }) — draw(t, elapsedMs) を fps でサンプリング。duration は必須。
const toss = straightAhead((t) => ({ x: t * 200, y: -300 * t + 300 * t * t }), { duration: 600 });
const nod = poseToPose([{ rotate: 0 }, { rotate: -6 }, { rotate: 0 }], { duration: 250 });

5. Follow through & overlapping action

本体が止まっても髪や服は慣性で行き過ぎてから落ち着き(follow through)、体の各部は少しずつずれて動く(overlapping action)。UI では到着時の小さなオーバーシュートや、リスト項目の時間差入場として現れる。

  • followThrough(spec, { bounce = 0.3 }) — 最終区間をスプリングで駆動し、終端を行き過ぎて戻らせる。
  • overlap(spec, count, { drag = 0.1, each = 30, cap = 50, total = 300, from = "first" }) → MotionSpec[]。各パートを staggerDelays の間隔でずらし、起点から n ステップ後ろのパートに bounce = drag × n(上限 0.8)の follow through を付ける(staggerDistances(count, from) が n を返す)。
  • spring({ duration = 400, bounce = 0.2 })(知覚パラメータ)または spring({ stiffness, damping, mass })(物理パラメータ。どちらかを指定すると stiffness 既定 170、damping 既定 26、mass 既定 1)。velocity も指定可。返り値はイージング関数で、.duration に静定時間(ms)を持つ。
  • useCascade(input = "rise", { trigger, ...OverlapOptions }) — コンテナの子要素を順に入場させる。trigger が変わると再生し直す(cap などのオプションを変えただけでは再生しない)。
const items = Array.from(list.children) as HTMLElement[];
const specs = overlap(enter("rise"), items.length, { each: 40 });
await playAll(items.map((element, i) => ({ element, spec: specs[i]! })));

6. Slow in & slow out

現実の物体は動き出しで加速し、止まる前に減速する。キーポーズ付近にコマを多く置くことで自然な重さが出る。UI では空間的な動きには必ずイージングを付け、linear は進捗表示だけに使う。

easings:

| 名前 | 値 | 用途 | | --- | --- | --- | | linear | "linear" | プログレスバー・スピナー・スクラブのみ | | inOut | cubic-bezier(0.65, 0, 0.35, 1) | 画面内の2状態間の移動 | | out | cubic-bezier(0.22, 1, 0.36, 1) | 入場 | | in | cubic-bezier(0.55, 0, 1, 0.45) | 退場 | | anticipate | cubic-bezier(0.36, 0, 0.66, -0.56) | 一度後ろに下がってから進む | | overshoot | cubic-bezier(0.34, 1.56, 0.64, 1) | 行き過ぎて戻る |

cubicBezier(x1, y1, x2, y2) は CSS の cubic-bezier() と同じ曲線を JS 関数として返す。Easing には CSS 文字列と JS 関数のどちらも渡せ、resolveEasing で関数に解決できる。

play(panel, { duration: 200, easing: easings.out, frames: [{ y: 16, opacity: 0 }, { y: 0, opacity: 1 }] });

7. Arcs

生き物の動きは直線ではなく弧を描く。直線移動は機械的に見える。UI ではカートに飛び込むアイテムや FAB からの展開など、2点間の移動を緩やかな曲線にする。

  • arc(from, to, { bend = 0.2, orient = false, samples = 24, duration, delay, easing = easings.inOut }) — 二次ベジェ曲線上をサンプリング。bend は移動距離に対する膨らみの割合(正で画面上方向)。orient: true で進行方向に回転する。duration の既定は移動距離から travelDuration で算出。x / y 以外のキーは並行して補間される。
  • arcPoint(a, b, bend, t) — 同じ曲線上の点。
play(chip, arc({ x: 0, y: 0 }, { x: 320, y: -40, scale: 0.4, opacity: 0 }, { bend: 0.3, orient: true }));

8. Secondary action

主動作を補強する副次的な動き(歩きながら腕を振る等)。主動作より目立ってはいけない。UI ではボタンを押したときのアイコンの揺れ、待機中の小さな浮遊など。

  • secondaryAction(kind, { amplitude = 1, duration = 500, delay, iterations }) — kind は "wiggle" / "pulse" / "float" / "sway"。amplitude は 1 未満を推奨。
  • accompany(primary, kind, { attenuation = 0.6, lag = 0.15 }) — 主動作の lag 割合だけ遅れて始まり、残りの尺に収まる副次動作。
const main = jump();
play(button, main);
play(icon, accompany(main, "wiggle"));

9. Timing

同じ動きでもコマ数(時間)で重さや感情が変わる。UI では時間が応答性そのものであり、ユーザー起点の動きは短く保つ。

  • durations — instant: 100, fast: 150, base: 200, slow: 300, deliberate: 500(deliberate はオンボーディングなどシステム起点の演出用)。
  • duration(value, tempo = 1) — トークンまたは ms に tempo を掛けて丸める。
  • travelDuration(distancePx, { min = 150, max = 300, reference = 800 }) — 距離の平方根に比例(reference px で max に到達)。
  • staggerDelays(count, { each = 30, cap = MAX_STAGGER_MS, total = 300, from = "first" }) — 要素ごとの遅延。from は "first" / "last" / "center" / インデックス。1 ステップは min(each, cap, total / 最大距離)。cap(既定 50ms)を上げると派手なカスケードにできる(負なら RangeError)。
const ms = travelDuration(Math.hypot(dx, dy));
play(el, poseToPose([{ x: 0, y: 0 }, { x: dx, y: dy }], { duration: ms }));

10. Exaggeration

現実をそのまま写すと弱く見えるため、本質を強調して誇張する。UI ではブランドの性格に合わせて動きの振れ幅を強めたり、逆に控えめにしたりする。

  • exaggerate(target, factor, { rest = REST, keys }) — Pose または MotionSpec(全フレーム)の、rest からの偏差を factor 倍する。factor > 1 で誇張、< 1 で抑制、負数で反転。keys の既定は opacity 以外の全キー。
play(el, exaggerate(squashStretch(), 2));

11. Solid drawing

描かれたものに体積・重さ・奥行きを持たせ、一貫した3D空間に置く。UI では一つの光源を前提に、浮いた要素ほど影が大きく・柔らかく・薄くなるよう統一し、傾きは本物のパースペクティブで描く。

  • lift(level = 4) → { y: -level * 0.5, elevation: level }
  • tiltToward(point, maxDeg = 8) — point は要素に正規化した座標(左上 (-0.5, -0.5) 〜 右下 (0.5, 0.5))。
  • solid(spec, perspective = 800) — perspective(px) を付け、rotateX / rotateY / z を立体的に描画する。
  • shadowForElevation(elevation) — elevation から2層の box-shadow 文字列を作る(Pose.elevation が内部で使う)。
  • tiltable(el, { max = 8, perspective = 800, personality, reducedMotion }) — ポインタに追従する3Dチルト。離れるとスプリングで平らに戻る。
  • hoverable(el, { level = 6, pose, personality, reducedMotion }) — ホバーで浮き上がる(タッチでは発火しない)。
hoverable(card, { pose: { scale: 1.02 } });
tiltable(card, { max: 6 });
play(card, solid(poseToPose([{ rotateY: 0 }, { rotateY: 180 }])));

12. Appeal

観客が惹かれるキャラクターには一貫した個性がある。UI ではプロダクト全体が一つの「キャラクター」として動くよう、テンポ・誇張・弾性を一か所で決める。

Personality は name, tempo(尺の倍率), exaggeration(振れ幅の倍率), bounce(0〜0.95), anticipation(0〜1), squash(0〜0.5), guardrails(boolean。true で押下などを UI ガイドライン内に収める)を持つ。

| personalities | tempo | exaggeration | bounce | anticipation | squash | guardrails | | --- | --- | --- | --- | --- | --- | --- | | natural | 1 | 1 | 0.2 | 0.15 | 0.04 | true | | snappy | 0.8 | 0.9 | 0.1 | 0.08 | 0.03 | true | | calm | 1.25 | 0.7 | 0 | 0 | 0.02 | true | | playful | 1 | 1.4 | 0.45 | 0.25 | 0.08 | true | | bouncy | 0.95 | 1.7 | 0.55 | 0.3 | 0.12 | false | | cartoon | 1.15 | 2.4 | 0.65 | 0.45 | 0.22 | false |

  • definePersonality(overrides, base = "natural") — 範囲検証済みの個性を作る(tempo は 0.1〜4、exaggeration は 0〜4 など。範囲外は RangeError、guardrails が boolean でなければ TypeError)。name と guardrails は省略すると base から引き継ぐ。
  • resolvePersonality(input = "natural") — 名前または Personality を解決する。
  • MotionProvider — React サブツリー全体に個性を配る。レシピと振る舞い(pressable 等)はすべて personality オプションを受け取る。
const brand = definePersonality({ name: "brand", tempo: 0.9, bounce: 0.3 }, "snappy");

<MotionProvider personality={brand}>{/* ... */}</MotionProvider>;

React で個性オブジェクトを使う場合は、上のようにコンポーネントの外で一度だけ作ること(毎レンダー新しいオブジェクトを渡すとコンテキスト値が毎回変わる)。

レシピと原則の合成

レシピは複数の原則を組み合わせ、personality で調整済みの MotionSpec を返す。共通オプションは RecipeOptions: { personality, duration, distance }。

| レシピ | 内容 | 既定 | | --- | --- | --- | | enter(kind = "rise", options) | 入場。easings.out。"pop" はスプリングでオーバーシュート(bounce は max(personality.bounce, 0.25)) | 尺 base(200ms)、"pop" は slow(300ms)、いずれも × tempo。distance 12 | | exit(kind = "fade", options) | 退場。easings.in。"pop" は anticipation > 0 の個性なら一度膨らんでから消える | 尺 fast(150ms)× tempo。distance 12 | | jump({ height = 16, ... }) | しゃがみ → 伸びて離陸 → 頂点で滞空 → 着地で潰れ → 静止。origin: "50% 100%" | 尺 450ms × tempo | | shake({ distance = 6, ... }) | x 方向の減衰振動(エラー表示) | 尺 400ms × tempo | | pressDepth(personality) | 押下時の縮小量(guardrails: true なら 0.02〜0.05、false なら 0.02〜0.2 にクランプ) | squash × 0.75 | | settleSpring(personality) | 押下・ホバー解除用のスプリング | duration 350 × tempo、bounce は個性の値 |

TransitionKind: "fade" / "rise" / "drop" / "slideLeft" / "slideRight" / "zoom" / "pop"。

原則の関数はすべて MotionSpec → MotionSpec(またはポーズ)なので、そのまま合成できる。

import { anticipate, arc, exaggerate, followThrough, play, poseToPose, secondaryAction, solid } from "twelve-principles";

// 溜めてから動き、行き過ぎて落ち着く
const toss = followThrough(anticipate(poseToPose([{ x: 0 }, { x: 200 }])), { bounce: 0.4 });

// 弧を描く移動を 1.3 倍に誇張(REST 基準なので移動距離そのものも 1.3 倍になる)
const swoop = exaggerate(arc({ x: 0, y: 0 }, { x: 200, y: 0 }), 1.3);

// 3D の揺れを無限ループで
const idle = solid({ ...secondaryAction("sway", { amplitude: 0.5 }), iterations: Infinity });

play(el, toss);

派手な動き(Expressive)

既定値は UI ガイドラインに沿っているが、祝福・オンボーディング・ゲームなど動きそのものを見せたい場面では、次の 3 つで意図して外へ出られる。どれも既定では無効。

  • guardrails: false の個性 — bouncy / cartoon、または definePersonality({ guardrails: false, ... })。pressDepth の上限が 0.05 から 0.2 になり、押下が深く潰れる(bouncy は scale 0.91、cartoon は 0.835)。
  • スタッガーの cap — staggerDelays / overlap / useCascade に cap を渡すと 1 項目 50ms の上限を超えられる。total(既定 300ms)も効くので、必要なら一緒に上げる。
  • 派手なレシピ — 下表。どれも RecipeOptions を受け取り、尺は基準 × tempo、振れ幅は exaggeration に比例し、静止状態で終わる。

| レシピ | 内容 | 既定 | | --- | --- | --- | | rubberBand({ axis = "x", ... }) | 体積を保って伸び縮みを繰り返す(Squash & stretch + Follow through) | 尺 800ms × tempo | | jello(options) | skew の減衰振動 | 尺 900ms × tempo | | tada(options) | 縮んで溜め → 膨らんで左右に揺れる → 静止(Anticipation + Secondary action) | 尺 1000ms × tempo | | heartbeat(options) | 2 回の鼓動と間 | 尺 1300ms × tempo | | swing(options) | 上端(origin: "50% 0%")から吊られた振り子 | 尺 1000ms × tempo | | wobble({ distance = 25, ... }) | x と回転の減衰振動 | 尺 1000ms × tempo | | flip(options) | perspective 400 で Y 軸 1 回転(rotateY -360 → 0 なので静止で終わる) | 尺 1000ms × tempo | | bounceIn(options) | opacity 0・縮小から、bounce 0.5 以上のスプリングで登場 | 尺はスプリングの静定時間 | | fallIn({ height = 80, ... }) | 重力で落ちて 3 回跳ねる(straightAhead)。着地で潰れる | 尺は物理的な所要時間 × tempo |

expressive は名前からレシピを引く辞書(Record<ExpressiveRecipe, (options?) => MotionSpec>)。

import { exaggerate, rubberBand, staggerDelays, tada } from "twelve-principles";
import { MotionProvider, useMotion } from "twelve-principles/react";

const louder = exaggerate(tada(), 1.5); // tada の振れ幅をさらに 1.5 倍
const delays = staggerDelays(6, { each: 120, cap: 150, total: 900 }); // [0, 120, 240, 360, 480, 600]

function Trophy() {
  const { ref, play } = useMotion<HTMLDivElement>();
  return <div ref={ref} onClick={() => play((p) => rubberBand({ personality: p }))}>Trophy</div>;
}

// 祝福画面だけ cartoon にして、アプリ全体のガードレールは保つ
<MotionProvider personality="cartoon"><Trophy /></MotionProvider>;

reduced motion は派手なレシピにもそのまま効く(静止で始まり静止で終わるものは動かず、bounceIn / fallIn は opacity のフェードになる)。iterations: Infinity でループさせる場合は停止手段を用意すること。

アクセシビリティ

play / animateTo / setLayer / 各振る舞い / stage / React の MotionProvider はすべて reducedMotion を受け取る(ReducedMotion 型)。

| 値 | 挙動 | | --- | --- | | "auto"(既定) | prefers-reduced-motion: reduce のときだけ "fade"、それ以外は "full" | | "fade" | 終端の形状へ即座に移り、opacity だけを最大 150ms でクロスフェードする | | "skip" | 終端状態へ即座に移る(尺 0) | | "full" | 常にフル再生。意味を伝えるのに不可欠なモーションに限る |

prefersReducedMotion() で現在の設定を、reduceSpec(spec, "fade" | "skip") で置き換え後の spec を取得できる。pressable はキーボード(Enter / Space)にも反応し、hoverable / tiltable はタッチ入力を無視する。

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

  • ユーザー起点の動きは 300ms 以内(USER_INITIATED_MAX_MS = 300)。押下 100ms、ホバー 200ms、入場 200ms(pop は 300ms)、退場 150ms、travelDuration の上限 300ms。これらは tempo 倍されるため、calm(1.25)では上回ることがある。jump(450ms)と secondaryAction(500ms)は注意喚起・演出用。
  • スタッガーは 1 項目あたり 50ms 以内(MAX_STAGGER_MS = 50)。staggerDelays の each は cap(既定はこの値)でクランプされ、全体は total(既定 300ms)に収まる。
  • 入場は ease-out、退場は ease-in。 enter は easings.out、exit は easings.in を使い、退場のほうが短い。
  • linear は進捗表示専用。 空間的な動きの既定は easings.inOut(poseToPose / animateTo / arc)。
  • 押下の縮小は scale 0.95〜0.98。 pressDepth は guardrails: true の個性(natural / snappy / calm / playful)では 0.02〜0.05 にクランプする。bouncy / cartoon のような guardrails: false の個性では 0.2 まで深くなる。
  • オーバーシュートはスプリングで。 解除時の戻り(settleSpring)、followThrough、enter("pop") は spring() で行き過ぎて静定する。

ブラウザ対応

WAAPI と Animation.prototype.commitStyles() が必要(Chrome 84+、Firefox 75+、Safari 13.1+)。CSS のイージング文字列はそのまま WAAPI に渡し、JS のイージング(スプリングや cubicBezier など)は compile 時に約 60fps の線形キーフレームへ焼き込むため、CSS の linear() 関数には依存しない。

開発

npm run dev        # dev/ のデモを Vite で起動
npm test           # vitest run
npm run build      # tsup で dist/ を生成(ESM + CJS + 型定義)
npm run typecheck  # tsc --noEmit
npm run docs:dev   # 解説サイト(docs/、Astro + Starlight)を起動
npm run docs:build # 静的サイトを docs/dist/ に出力(検索インデックス込み)
npm run skill:install -- <skills-dir>  # エージェント用スキルをコピー(例: ../my-app/.claude/skills)

エージェント用スキル

skills/ui-motion-principles/ は、コーディングエージェント(Claude Code など)にこのライブラリの使い方を教える Agent Skill です。解説サイトの「エージェント用スキル」ページで全ファイルを公開し、/skill/ui-motion-principles.zip として配布しています(ビルド時にこのディレクトリから生成)。npm パッケージにも同梱されるので、cp -r node_modules/twelve-principles/skills/ui-motion-principles .claude/skills/ でも導入できます。テンプレートの .tsx は npm run typecheck の対象です。

参考資料