GODRICH

チュートリアル UI デザイン9選 — コピペOKのツアー部品

チュートリアル ui デザインは、登録したばかりの人に「次にどこを押せばいいか」を画面上で直接指し示す部品です。チェックリスト・スポットライト・ステップ式ツールチップなど、形はだいたい決まっているので、選んで貼るだけで済みます。実際に動かして確かめた9種をまとめました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並び順は人気順ではなく、新規ユーザーが実際に出会う順番です。最初の画面でウェルカムモーダル(01)が開き、残りの作業はチェックリスト(02)が引き受けます。押すべきボタンが見つからないときはコーチマーク(03)が画面を暗くして一か所だけを照らし、順番に説明したいときはステップ式ツールチップ(04)が進む・戻るで案内します。どこまで来たかは進捗ストリップ(05)が示し、使われていない機能の横にはヒントの吹き出し(06)とパルスバッジ(07)が付きます。まだ何もないリストには空の状態カード(08)が代わりに置かれ、最後は紙吹雪(09)で締めます。

01ウェルカムモーダルのステッパー

モーダルが下から18px浮き上がって開き、下に並ぶ3つのドットのうち現在の段階の1つだけが横に伸びてピル型になります。ドットを広げているのが width ではなく scaleX なので、周りの文字はずれません。

translateY 18px점→알약radius 16
.dots i {
  width: 20px; height: 8px;
  border-radius: $r-pill;
  background: #cbd5e1;
  transform: scaleX(.4);
  animation-name: step;
  animation-duration: $duration;
  animation-timing-function: steps(1, end);
  animation-iteration-count: infinite;
}
.d2 { animation-delay: calc(#{$duration} / 3); }
@keyframes step {
  0%     { transform: scaleX(1); background: $color; }
  33.33% { transform: scaleX(.4); background: #cbd5e1; }
}

02チェックリストの進捗カード

残った2行が半周期ずれてチェックされ、ラベルには左から取り消し線が引かれます。カード上部のバーも同じリズムで、50%から75%を経て100%まで伸びます。

scaleX 취소선50%→75%radius 16
.bar i {
  position: absolute; inset: 0;
  background: $color;
  transform: scaleX(.5);
  transform-origin: left center;
}
@keyframes fill {
  0%, 10%   { transform: scaleX(.5); }
  40%, 55%  { transform: scaleX(.75); }
  85%, 100% { transform: scaleX(1); }
}
@keyframes strike {
  0%, 18%   { transform: scaleX(0); }
  44%, 100% { transform: scaleX(1); }
}

03コーチマークのスポットライト

画面を覆う暗幕に穴が1つだけ空いて対象を照らし出し、その穴が左のボタンから右のカードへ移動します。暗幕は別に用意した要素ではなく、穴そのものにかけた box-shadow を9999pxまで広げたものです。

box-shadow 스포트라이트구멍 이동radius 12
.spot {
  position: absolute;
  left: 12px; top: 12px;
  width: 120px; height: 40px;
  border-radius: $r-control;
  box-shadow: 0 0 0 9999px rgba(23, 20, 26, .74);
  transform-origin: left center;
}
@keyframes move {
  0%, 18%  { transform: translateX(0) scaleX(1); }
  50%, 68% { transform: translateX(124px) scaleX(.8333); }
  100%     { transform: translateX(0) scaleX(1); }
}

04ステップ式ツールチップツアー

切り替わるたびに「次へ」ボタンだけが沈み、それに合わせてツールチップが横にスライドしますが、最後の1枚から最初に戻るときも巻き戻りません。トラックの末尾に1枚目の複製を置いてあるので、動きは常に同じ方向へ進み続けます。

첫 장 복제translateX 단계radius 12
// スライドは3枚ではなく4枚。末尾の1枚は1枚目の複製
.track { display: flex; width: 400%; }
.slide { width: 25%; flex: 0 0 25%; }
@keyframes walk {
  0%,   16% { transform: translateX(0); }
  25%,  41% { transform: translateX(-25%); }
  50%,  66% { transform: translateX(-50%); }
  75%, 100% { transform: translateX(-75%); }
}

05進捗ストリップ

5つの区画が左から0.2秒ずつ遅れて埋まり、いま埋まった区画にだけ白い膜が一瞬かかって消えます。区画は均等割りではなく47pxの固定幅なので、埋まった割合が計算した値とずれません。

5칸 세그먼트채워진 칸 점등radius 999
.seg { flex: 0 0 auto; width: 47px; height: 8px; overflow: hidden; }
.fill {
  position: absolute; inset: 0;
  background: $color;
  transform: scaleX(0);
  transform-origin: left center;
  animation-name: fill;
  animation-duration: $duration;
}
.s2 .fill { animation-delay: .2s; }
.s3 .fill { animation-delay: .4s; }
@keyframes fill {
  0%        { transform: scaleX(0); }
  16%, 100% { transform: scaleX(1); }
}

06ヒントの吹き出し

吹き出しが対象のボタンの上で4pxだけゆっくり上下に揺れます。下向きの三角のしっぽを別要素ではなく ::after にしてあるため、揺れている間も本体から離れません。

translateY 4px꼬리 일체형radius 12
.hint {
  position: relative;
  background: $color;
  transform: translateY(0);
}
.hint::after {
  content: "";
  position: absolute;
  left: 50%; bottom: -6px;
  width: 12px; height: 12px;
  background: $color;
  transform: translateX(-50%) rotate(45deg);
}
@keyframes nudge {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}

07新機能ハイライトのパルスバッジ

リングがドットの2.4倍の大きさまで広がり、背後で消えていきます。ドットを position: absolute でレイアウトの流れから外してあるので、リングがどれだけ大きくなってもメニューの行間は動きません。

scale 1→2.4레이아웃 불변radius 50%
.dot {
  position: absolute;
  right: 10px; top: 8px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: $color;
}
.ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  background: $color;
  opacity: .55;
}
@keyframes pulse {
  0%        { opacity: .55; transform: scale(1); }
  70%, 100% { opacity: 0;   transform: scale(2.4); }
}

08空の状態の案内カード

破線の枠が一方向にゆっくり流れます。CSSの border-style: dashed はアニメーションさせられないため、SVGの rectstroke-dashoffset を動かし、1周期でちょうど1目盛り(10+8=18)だけ流して継ぎ目をなくしました。

점선 흐름dashoffsetradius 16
// 枠は border: dashed ではなく stroke-dasharray: 10 8 を持つ SVG rect
.edge rect {
  animation-name: flow;
  animation-duration: $duration;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes flow {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: -18; }
}

09完了お祝いの紙吹雪

オレンジのスタンプが一度沈んで跳ね返り、その上から12個の紙片が落ちてきます。紙片ごとに横位置・色・傾き・開始タイミングがすべて違うので、同じ瞬間にまとめてばらまかれたようには見えません。

12조각 시차scale 0.8→1.12radius 50%
.b {
  position: absolute;
  top: -12px;
  width: 6px; height: 10px;
  border-radius: $r-xs;
  opacity: 0;
}
.b3 { left: 26%; animation-delay: .21s; --r: 61deg; }
.b8 { left: 69%; animation-delay: .4s;  --r: -29deg; }
@keyframes fall {
  0%   { opacity: 0; transform: translateY(0) rotate(0deg); }
  12%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(168px) rotate(var(--r)); }
}

どこで壊れるか — 指定した時間差が黙って消える

01と05を最初にレンダリングしたとき、時間差がまったく効いていませんでした。3つのドットが同時に点灯し、5つの区画が一斉に埋まったのです。原因は、animation-delay を詳細度の低い規則に分けて書いておきながら、共通の規則では animation: fill 2s ease-out infinite というショートハンドを使っていたことです。ショートハンドは省略した個別プロパティをすべて初期値に戻すため、詳細度の高い共通規則が勝った時点で遅延が 0s にリセットされます。ブラウザで計算値を読むと、5つの区画すべてで animationDelay0s になっていました。共通規則を animation-nameanimation-durationanimation-timing-functionanimation-iteration-count の個別プロパティに書き直したところ、時間差が戻り、05で動いたフレーム数は23枚中8枚から17枚に増えました。遅延を別の規則やインラインの style から渡す予定があるなら、その要素には animation ショートハンドを使わないでください。

2つ目に引っかかったのは、05の区画の幅でした。最初は5つの区画を flex: 1 1 0 の均等割りにして、埋まる部分だけを scaleX で伸ばしていたのですが、自動チェックの段階でこの組み合わせが弾かれました。均等割りは各区画の実際の描画幅をコンテナ側の計算に任せるので、埋まる比率を固定値で決める作りとは相性が悪く、いつずれてもおかしくありません。78pxのつもりで書いた埋まり方が実際には62pxの区画の上で動けば、表示される割合はまるごと狂います。そこで区画を flex: 0 0 auto の47px固定幅に変えました。480pxのデモ画面ではどちらも同じに見えますが、ずれは画面が狭くなるときではなく、埋まり方を書くときに想定した幅と実際にレンダリングされた幅が違うときに現れます。

09の紙吹雪は別の理由で一度作り直しました。12個の紙片を箱の上端の外(top: -12px)から出発させて168px下まで落としたところ、紙片の境界ボックスがデモ画面の高さにそのまま加算され、9マスのグリッドの09番のマスにスクロールバーが出てしまったのです。画面に収まっているかを測る基準は scrollHeight で、transform で押し出された要素もこの値に含まれます。overflow: hidden であれ clip であれ、どこかの親要素で実際に切り取らないかぎりこの数値は減りません。紙片を入れる .sceneoverflow: hidden をかけたところ、測定値は元に戻りました。

アイコンは手描きせず、Phosphor Duotone の8種をダウンロードしてSVGをそのままインラインで埋め込みました。最初に試したIconifyのURLは8回とも403を返し、jsdelivrの @phosphor-icons/core のパスに変えたら一度にすべて取得できました。HTMLに直接書き込んであるので、実行時に外部へ通信する箇所は一つもありません。<div> を重ねたり border-radius で似せたりしたアイコンは少し拡大しただけでずれますが、本物のSVGは width を変えるだけで済みます。最後に9種の動きを測ると、07のパルスバッジが累積面積0.12%でもっとも小さい値でした。8pxのドットとその背後で広がるリングだけなので、画面上で実際に変化する面積がそれだけ狭いのです。zipを解凍するときのパスワードは gs9cg6k2 で、ダウンロードしたファイルにそのまま入力すると9つのフォルダがまとめて開きます。

アクセシビリティ

明るいトーンのチュートリアル画面で最初に崩れるのは、動きではなく文字のコントラストです。パステルやオレンジは見た目がよくても、白背景では本文用のコントラスト基準に届きません。そのため9種すべてで文字色はインク(#17141a)かスレート(#475569)に固定し、パステルは背景・枠線・ドットにだけ使いました。以下は、実際に使った組み合わせを WCAG 2.1 の相対輝度の式で計算した値です。

文字 背景 比率 本文(4.5:1)
インク #17141a 白カード #ffffff 18.24:1 合格
インク #17141a ミントの吹き出し #4cd4a6 9.79:1 合格
スレート #475569 白カード #ffffff 7.58:1 合格
ブランドブルー #2f6df6 白カード #ffffff 4.53:1 合格(ぎりぎり)
#ffffff オレンジのスタンプ #ff4d1f 3.32:1 不合格 — 大きい文字のみ
ミント #4cd4a6 白カード #ffffff 1.86:1 不合格 — 装飾専用

09の「すべて完了」をオレンジのスタンプの中に白文字で置かず、背景の上にインクで置いたのはこの3.32:1が理由です。動きの側は、9種すべてが prefers-reduced-motion: reduce で最終状態のまま止まります。03は穴が最初の対象に止まったまま、05は5つの区画が埋まったまま、09は紙片が落ちずスタンプだけが残ります。それぞれの意味は MDN の prefers-reduced-motionWCAG のコントラスト基準 に整理されています。

FAQ

コーチマークの暗い幕には、画面全体を覆うdivを別に作る必要がありますか

いいえ。03のように穴になる要素そのものに box-shadow: 0 0 0 9999px をかければ、その外側がまとめて幕になります。親に overflow: hidden をかけておけば、枠の外へ漏れることもありません。幕用の要素を別に置くと、穴を動かすたびに2つの要素の位置を合わせ続けることになり、かえってずれやすくなります。

複製を使わず、最後から最初へそのまま戻してはいけませんか

動くことは動きます。ただし画面は1枚進むのではなく、3枚分あからさまに巻き戻ります。04はトラックの末尾に1枚目の複製を置いて常に前へ進むので、一周して0%に戻っても、そこに見えているのは同じ1枚目の絵で継ぎ目が分かりません。似たループの作り方は CSS ツールチップ吹き出し でも取り上げました。

進捗バーを width で伸ばしてはいけませんか

どちらでも動きます。ただし width はフレームごとにレイアウトを計算し直すため、02のように文字行が何行もあるカードでは、バーが伸びる間に下の行まで揺れることがあります。transform: scaleX()transform-origin: left center は合成の段階で処理されるため、周囲のレイアウトを押しません。バー自体については CSS 進捗バー・ゲージ に詳しく書きました。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。