コンテンツにスキップ

Staging(演出)

画面の中で今いちばん大事なものを一つに決め、それ以外を一歩下がらせることで、視線は迷わず主役に向かいます。twelve-principles では、主役と周囲のポーズを返す stagingPoses と、それを DOM に適用する stage / useStage を提供しています。

LIVE
A〜D のタイルをクリックすると、そのタイルが浮き上がり、残りが薄く・ぼやけ・少し小さくなります。同じタイルをもう一度押すと解除、別のタイルを押すと主役が切り替わります。

Staging は、演劇や映画の「演出」と同じ考え方です。観客が一度に理解できることは一つだけなので、アイデア、感情、動作のどれを伝えるにしても、それが誤解なく見えるように画面を組み立てます。

手段は構図、カメラの位置、光と影、そして動きです。主役を画面の重心に置き、背景は暗く・単純にし、同じ瞬間に他のものを大きく動かさない。シルエットだけで動作が読めるように、ポーズを横向きに見せることもあります。逆に、背景で別のキャラクターが派手に動いていれば、どれほど良い演技でも見逃されます。

UI での Staging は、「今ユーザーが注目すべき要素」を視覚的に前景へ出すことです。選択中のカード、編集中の行、オンボーディングで説明中のボタンなどが該当します。stagingPoses の既定値は次のとおりです。

対象 変化 既定値
主役(focus) わずかに拡大、影で浮かせる scale 1.02、elevation 12
周囲(surroundings) 薄く、ぼかし、わずかに縮小 opacity 0.5、blur 2px、scale 0.98

主役を 2% 大きく、周囲を 2% 小さくするだけでも、奥行きの差が 4% 生まれ、「手前と奥」として読まれます。適用には durations.slow(300ms)× tempo の ease-out、解除には durations.base(200ms)× tempo の ease-in-out を使います。入るときはゆっくり落ち着き、戻るときは素早く、という非対称です。

向いている場面:

  • 複数の選択肢から一つを選んでいる状態(料金プラン、配送方法)。
  • オンボーディングやツアーで特定の UI を説明しているとき。
  • インライン編集中の行やカード。

避けるべき場面:

  • 常時表示のナビゲーションや、頻繁に切り替わるタブ。周囲が毎回暗くなるのは煩わしく、読みにくくなります。
  • 周囲の情報を読み比べる必要があるとき。opacity 0.5 ではテキストのコントラストが大きく下がります。
function stagingPoses(options?: StagingOptions): StagingPoses;
interface StagingPoses {
focus: Pose;
surroundings: Pose;
}

主役と周囲のポーズだけを返す純関数です。DOM には触れません。戻り値は focus: { scale: 1 + recede, elevation: lift }surroundings: { opacity: 1 - dim, blur, scale: 1 - recede } です。

オプション 既定値 説明
dim number 0.5 周囲を薄くする量(0〜1)。範囲外は RangeError
blur number 2 周囲のぼかし(px)。
recede number 0.02 周囲を縮める割合。主役は同じ割合だけ拡大する。
lift number 12 主役の elevation(影の高さ)。
function stage(focus: HTMLElement, options?: StageOptions): StageHandle;
interface StageHandle {
release(): Promise<void>;
}

focus を前へ出し、周囲を下げます。StageOptionsStagingOptions のすべて(dim / blur / recede / lift、既定値は上の表と同じ)に加えて、次を受け取ります。

オプション 既定値 説明
surroundings readonly HTMLElement[] focus の兄弟要素 一歩下がらせる要素。既定は親要素の子のうち focus 以外の HTMLElement
personality PersonalityInput "natural" 個性。tempo が適用・解除の尺に掛かる。
reducedMotion ReducedMotion "auto" reduced motion の扱い。

動作の詳細:

  • 主役は拡大すると隣の要素に重なるため、描画順を前に出します。計算済みの positionstatic ならインラインで position: relative を付け、インラインの z-index10 にします。
  • 変化は名前付きレイヤー "stage" に設定されます(setLayer)。同じ要素の "hover" / "press" / "tilt" レイヤーとは合成され、互いを上書きしません。たとえば hoverable の浮き上がり(既定 elevation 6)と重なると、主役の elevation は 18 になります。
  • 適用は duration("slow", tempo)easings.out、解除は duration("base", tempo)easings.inOut です。
  • release()"stage" レイヤーを外して元の状態へ戻し、そのモーションが終わってから z-indexpositionstage() 呼び出し前のインライン値に戻します。戻る途中も主役は前面に残ります。
  • release() は何度呼んでも同じ Promise を返します。
function useStage<T extends HTMLElement = HTMLElement>(
active: boolean,
options?: Omit<StageOptions, "personality" | "reducedMotion">,
): RefCallback<T>;

activetrue の間だけ、ref を付けた要素をステージングする React フックです。personalityreducedMotionMotionProvider から受け取ります。

  • activefalse になるか、アンマウントされると release() します。
  • dim / blur / recede / lift は値で比較されるので、インラインのオブジェクトリテラルを渡しても毎レンダーで再ステージされません。値が変わったときだけ解除して掛け直します。
  • surroundingsステージングの開始時に一度だけ読まれます。インラインで配列を作って渡しても再ステージは起きませんが、ステージング中に配列の中身を変えても反映されません。

カードのグリッドで、クリックしたカードを主役にします。同じカードをもう一度押すと解除、別のカードを押すと切り替えます。

import { stage, type StageHandle } from "twelve-principles";
const grid = document.querySelector<HTMLElement>(".plan-grid")!;
let staged: { card: HTMLElement; handle: StageHandle } | undefined;
grid.addEventListener("click", (event) => {
const card = (event.target as HTMLElement).closest<HTMLElement>(".plan-card");
if (!card) return;
const previous = staged;
staged = undefined;
previous?.handle.release();
previous?.card.setAttribute("aria-pressed", "false");
if (previous?.card !== card) {
staged = { card, handle: stage(card, { dim: 0.4 }) };
card.setAttribute("aria-pressed", "true");
}
});

兄弟要素以外を下げたいときは surroundings を明示します。ツアーで特定のボタンを説明する例です。ぼかしは要素数が多いと描画コストが高いので切っています。

import { stage } from "twelve-principles";
export async function explain(target: HTMLElement, done: Promise<void>) {
const panels = Array.from(document.querySelectorAll<HTMLElement>(".panel"));
const handle = stage(target, {
surroundings: panels.filter((panel) => !panel.contains(target)),
dim: 0.7,
blur: 0,
});
await done; // ユーザーが「次へ」を押すまで待つ
await handle.release();
}

stage の動きではなくポーズだけが欲しいときは stagingPoses を使い、自分でレイヤーに設定します。

import { setLayer, stagingPoses } from "twelve-principles";
const { focus, surroundings } = stagingPoses({ dim: 0.8, blur: 0, recede: 0 });
const sidebar = document.querySelector<HTMLElement>(".sidebar")!;
const comments = document.querySelector<HTMLElement>(".comments")!;
export function setTheaterMode(player: HTMLElement, on: boolean) {
setLayer(player, "theater", on ? focus : null);
for (const el of [sidebar, comments]) setLayer(el, "theater", on ? surroundings : null);
}
  • Solid Drawing — 主役の浮き上がりは elevation による影で描かれます。影の大きさ・柔らかさは他の浮いた要素と同じ光源で統一されます。
  • Slow In & Slow Out — 適用は easings.out、解除は easings.inOut
  • Timing — 適用 300ms、解除 200ms(いずれも durations のトークン × tempo)。
  • Secondary Action — 主役を立てている間は、周囲で副次的な動きを走らせないこと。視線が割れます。
  • Appealpersonalitytempo で、ステージングの速さがプロダクト全体と揃います。
  • アクセシビリティreducedMotion の挙動と、視覚的な強調を補う ARIA。