GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

オープニング アニメーション css 9選 — イントロ アニメーション 入門

オープニング アニメーション css とは、ページが開いた最初の1〜2秒だけ画面を覆い、やがて退いて本来の画面を引き渡す演出のことです。スキップできてこそイントロ — 9つすべてにボタンと Esc と再訪の記録が備わり、その場で終わらせられます。どのデモもこのページの中で今すぐ動きます。

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

並びは人気の順ではなく、覆いが開く単位が画面の大きな面から細い一筆へ移り、また画面全体へ戻る順です。画面を半分ずつに割る幕がまず退き(01)、散らばった欠片が集まってロゴになり(02)、ここからは一文字(03)・円ひとつ(04)・数字3つ(05)・線一本(06)と主役がだんだん小さくなります。07からは再び画面全体が動いて、左右に割れ(07)、見出しがずれては戻り(08)、色帯が走り抜けます(09)。9つすべてでアニメーションのない基本状態がそのままイントロの終わった画面なので、スキップはクラスを入れ替えるだけの小さな仕事になります。導入の隣人として、待つ間の画面を扱うローディング 画面 デザイン 9選と、画面と画面の間を扱うcss ページ遷移 アニメーション 9選がこの編の両隣です。

01カーテンリフト

画面を覆う二枚の幕が上下に分かれて退き、見出しが一拍遅れて浮かび上がる、ブランドLPの最初の入口に使うイントロです。

カーテンリフト
新しいタブで開く

01 · カーテンリフト — 計測 累積面積 99.947% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateYanimation-fill-mode$ease-spring

基本状態を完了画面にしておく

// ── 二枚の幕。基本の transform は「開いた」位置(画面の外)。前縁にクリームの差し色を置き、退く向きを示す
.cl__veil { position: absolute; left: 0; right: 0; height: 50.5%; background: $color; z-index: 2; }
.cl__veil--top { top: 0; transform: translateY(-102%); }
.cl__veil--bot { bottom: 0; transform: translateY(102%); }
.cl__veil--top {
  animation-name: clTopOnce; animation-duration: $duration; animation-timing-function: $easing;
  animation-delay: .08s; animation-fill-mode: both;
}
@keyframes clTopOnce { from { transform: translateY(0); } to { transform: translateY(-102%); } }
@keyframes clBotOnce { from { transform: translateY(0); } to { transform: translateY(102%); } }
@keyframes clRiseOnce { from { transform: translateY(24px); } to { transform: none; } }

幕の基本の transform はすでに画面の外なので、アニメーションを切った瞬間がそのまま完了画面になります。閉じたままの最初の場面は from キーフレームと animation-fill-mode: both が代わりに埋めます。

スキップと再訪を一つの関数で受ける

// イントロを完了状態へ送る — アニメーションを切れば基本状態(開いた画面)がそのまま現れる
function finish(announce) {
  root.classList.remove('is-demo');
  root.classList.add('is-done');
  try { sessionStorage.setItem(KEY, '1'); } catch (e) {}
  if (announce) live.textContent = 'イントロをスキップしました';
}
document.getElementById('cl-skip').addEventListener('click', function () { finish(true); });
document.addEventListener('keydown', function (e) {
  if (e.key === 'Escape') finish(true);
});

クラス2つを入れ替える関数1つが、ボタン・Esc・再訪の3つの経路をまとめて受け持ちます。この項目のループは累積面積 99.947%・動いたフレーム 19/23・アニメーション5つで測りました。

02ロゴ組み立て

四方から飛んでくる4つの欠片が時差で中央に集まり、最後の欠片が着いた瞬間にロゴがひとつになる、ロゴを先に見せる会社紹介向けのイントロです。

ロゴ組み立て
新しいタブで開く

02 · ロゴ組み立て — 計測 累積面積 21.906% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

clip-pathtransform-originanimation-delay

円ひとつを4分割して欠片を作る

.la__q {
  position: absolute; inset: 0; border-radius: 50%;
  transform-origin: 50% 50%;   // 回転軸 = 円の中心 = 欠片がロゴに着く点
}
.la__q--tl { background: $subject-ink;    clip-path: inset(0 51% 51% 0); }
.la__q--tr { background: $color;          clip-path: inset(0 0 51% 51%); }
.la__q--br { background: $chart-3;        clip-path: inset(51% 0 0 51%); }
.la__q--bl { background: $stage-orange;   clip-path: inset(51% 51% 0 0); }

4つの欠片は同じ場所に重ねた同じ大きさの円で、clip-path: inset がそれぞれの見える四分円だけを残します。4つが定位置に揃うと、51%で取った余白が細い隙間として見えます。

遅延を一段ずつずらして到着順を作る

.la__q--tr {
  animation-name: laInTr; animation-duration: $duration; animation-timing-function: $easing;
  animation-delay: .08s; animation-fill-mode: both;
}
@keyframes laInTl { from { transform: translate(-150px, 0) rotate(-10deg); } to { transform: none; } }
@keyframes laInTr { from { transform: translate(0, -150px) rotate(8deg); } to { transform: none; } }
@keyframes laInBr { from { transform: translate(150px, 0) rotate(10deg); } to { transform: none; } }
@keyframes laInBl { from { transform: translate(0, 150px) rotate(-8deg); } to { transform: none; } }

欠片ごとに出発方向と傾きが違い、遅延だけが0.08秒ずつずれています。累積面積は 21.906% で画面の5分の1ほどしか使わないのに、動いたフレームは 19/23 まで出ます。

03一文字ずつのマスク

文字ごとに切り取り箱をかぶせ、下から一文字ずつ押し上げて床から生えるように見せる、見出し一行が主役のヒーロー向けイントロです。

一文字ずつのマスク
新しいタブで開く

03 · 一文字ずつのマスク — 計測 累積面積 6.092% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

overflow: hidden--ianimation-delay

箱がマスクを兼ねて文字は中だけで見える

.tt__row { display: flex; gap: $sp-1; margin: 0; }
.tt__box { display: block; overflow: hidden; }
.tt__ch {
  display: block; color: $color;
  font-weight: 800; font-size: clamp(26px, 7vw, 36px); line-height: 1.15;
}
.tt__ch {
  animation-name: ttChOnce; animation-duration: $duration; animation-timing-function: $easing;
  animation-delay: calc(var(--i) * .05s); animation-fill-mode: both;
}
@keyframes ttChOnce { from { transform: translateY(112%); } to { transform: none; } }

文字を112%下げて置くと overflow: hidden の箱の外になるので、まったく見えなくなります。この作りには半透明に透ける中間状態が入る余地がありません。

順番はマークアップが変数で渡す

<h1 class="tt__row" aria-label="GODRICH">
  <span class="tt__box" aria-hidden="true" style="--i:0"><span class="tt__ch">G</span></span>
  <span class="tt__box" aria-hidden="true" style="--i:1"><span class="tt__ch">O</span></span>
  <span class="tt__box" aria-hidden="true" style="--i:2"><span class="tt__ch">D</span></span>
  <span class="tt__box" aria-hidden="true" style="--i:3"><span class="tt__ch">R</span></span>
  <span class="tt__box" aria-hidden="true" style="--i:4"><span class="tt__ch">I</span></span>
  <span class="tt__box" aria-hidden="true" style="--i:5"><span class="tt__ch">C</span></span>
  <span class="tt__box" aria-hidden="true" style="--i:6"><span class="tt__ch">H</span></span>
</h1>

一文字ずつを aria-hidden で隠し、行全体に aria-label を付けて、読み上げではひとつの単語として聞こえます。文字が順番に上がるおかげで動いたフレームは 21/23 と、9つの中でいちばん細かくなりました。

04円形マスクワイプ

中央の一点から始まった円が画面外まで広がって覆いを消し、下に敷かれていた内容をそのまま現す、ポートフォリオの最初の切り替え向けイントロです。

円形マスクワイプ
新しいタブで開く

04 · 円形マスクワイプ — 計測 累積面積 97.503% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

clip-path: circle$ease-outanimation-fill-mode

円が大きくなるのではなく覆いが消える

// 最初の画面 — clip-path の円がこの要素をマスクする。背景は不透明なので、円の中では覆いを完全に隠す
.iw__site {
  position: absolute; inset: 0; z-index: 2;
  display: grid; place-items: center;
  box-sizing: border-box; padding: $sp-4;
  background: $stage-paper;
}
@keyframes iwOpen {
  0% { clip-path: circle(0% at 50% 50%); }
  62% { clip-path: circle(75% at 50% 50%); }
  100% { clip-path: circle(75% at 50% 50%); }
}

動いているのはインクの覆いではなく、その上に載せた最初の画面の clip-path 一行です。円の内側は不透明な紙色なので、広がる分だけ覆いが切り取られていきます。

半径75%が四隅に届く計算

// 75% ならこの舞台の四隅まで届く(480×300 の基準半径 400px、四隅までの距離 283px)
.iw.is-demo .iw__site {
  animation-name: iwOpen;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}

circle() のパーセントは縦横を合わせた基準半径に対する値なので、480×300 の舞台で 75% は 300px になります。中心から四隅までの 283px より長いため四隅まで消え、累積面積は 97.503% と画面のほぼ全体です。

05カウントダウン導入

3・2・1 が1フレームで切り替わり、数字ごとに大きさが一段縮んで、数える間に画面が近づいてくる、イベントや配信の開始画面向けイントロです。

カウントダウン導入
新しいタブで開く

05 · カウントダウン導入 — 計測 累積面積 10.317% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

steps(1, end)font-variant-numericscale

数字3つを同じ枡に重ねる

.cd__n {
  grid-area: 1 / 1;
  color: $color;
  font-weight: 800; font-size: clamp(72px, 24vw, 104px); line-height: 1;
  font-variant-numeric: tabular-nums;   // 3・2・1 の幅が揃い、コマのたびに中心が揺れない
  opacity: 0;                            // 基本(完了状態)には数字がない
}
@keyframes cdN3   { 0% { opacity: 1; } 14% { opacity: 0; } 100% { opacity: 0; } }
@keyframes cdN2   { 0% { opacity: 0; } 14% { opacity: 1; } 28% { opacity: 0; } 100% { opacity: 0; } }
@keyframes cdN1   { 0% { opacity: 0; } 28% { opacity: 1; } 42% { opacity: 0; } 100% { opacity: 0; } }

3つの数字を grid-area: 1 / 1 で同じ枡に重ね、見せるひとつだけを選んで点けます。幅の揃った数字なので、入れ替わっても中心線は揺れません。

ポップを数字ごとに分ける理由

// ポップは数字ごとに専用の keyframes 1つ — 同じプロパティ(transform)に重ねるとリストの後ろが勝ち、順序がずれる
.cd.is-demo .cd__n--3 .cd__pop { animation-name: cdPop3; }
.cd.is-demo .cd__n--2 .cd__pop { animation-name: cdPop2; }
.cd.is-demo .cd__n--1 .cd__pop { animation-name: cdPop1; }
@keyframes cdPop3 { 0% { transform: scale(1.5); } 12% { transform: scale(1); } 100% { transform: scale(1); } }
@keyframes cdPop2 { 0%, 14% { transform: scale(1.5); } 26% { transform: scale(1); } 100% { transform: scale(1); } }
@keyframes cdPop1 { 0%, 28% { transform: scale(1.5); } 40% { transform: scale(1); } 100% { transform: scale(1); } }

コマ送りとイージングをひとつのキーフレームに混ぜないため、点灯は数字の要素が、大きさのポップはその中の子が分担します。アニメーションは10入りですが、動いたフレームは 14/23 と少なめです。

06ライン描画の枠

枠が一筆で描かれ、線が閉じると中の文字が続いて点く、展示や雑誌の表紙のような入り方をするイントロです。

ライン描画の枠
新しいタブで開く

06 · ライン描画の枠 — 計測 累積面積 6.66% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

stroke-dasharraystroke-dashoffsetpathLength

pathLength 100 が長さ計算を消す

.ld__rect {
  fill: none; stroke: $color; stroke-width: 3;
  stroke-dasharray: 100;   // pathLength 100 基準 — 描ける最大の長さ
  stroke-dashoffset: 0;    // 初期値 0 = 線がすべて引かれた完成状態
}
@keyframes ldDraw {
  0% { stroke-dashoffset: 100; }
  56% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 0; }
}

<rect> に pathLength="100" を書いておくと、ブラウザが実際の周囲長を100へ換算してくれます。ダッシュの長さを測ったり、周囲長をスクリプトで取ったりする仕事がなくなります。

線が閉じてから文字を点ける

.ld.is-demo .ld__kicker,
.ld.is-demo .ld__title,
.ld.is-demo .ld__sub {
  animation-name: ldTextOn, ldTextRise;
  animation-duration: $duration, $duration;
  animation-timing-function: steps(1, end), $easing;
  animation-iteration-count: infinite, infinite;
  animation-fill-mode: backwards, backwards;
}
// 線が閉じる 56%(1.12s)から一つずつ — 上のラベル、見出し、サブ
.ld.is-demo .ld__kicker { animation-delay: 1.12s, 1.12s; }
.ld.is-demo .ld__title { animation-delay: 1.2s, 1.2s; }
.ld.is-demo .ld__sub { animation-delay: 1.28s, 1.28s; }

点灯と浮上をカンマで並べて、ひとつの要素がコマと曲線を同時に踏みます。線が閉じる1.12秒から0.08秒間隔でラベル・見出し・サブが順に入り、アニメーション7つ・動いたフレーム 18/23 になりました。

07スプリットスクリーン

左右の半分が反対方向へ抜けて中央の縦線が開き、隙間が画面幅まで広がると本文が収まる、製品詳細へ入る導入演出です。

スプリットスクリーン
新しいタブで開く

07 · スプリットスクリーン — 計測 累積面積 78.636% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateXwill-change$dur-enter

左右の半分が反対方向へ抜ける

.scr__panel {
  position: absolute; top: 0; bottom: 0; width: 50.5%; z-index: 2;
  display: flex; align-items: center;
  will-change: transform;          // チップ: will-change — 動く前に合成レイヤーへ上げる
}
.scr__panel--l {
  left: 0; justify-content: flex-end; padding-right: $sp-1;
  background: $color;
  transform: translateX(-102%);    // 自分の幅より少し外へ — 継ぎ目が見えないように
  animation-name: scrPanelLOnce; animation-duration: $duration;
  animation-timing-function: $easing; animation-iteration-count: 1; animation-fill-mode: backwards;
}
@keyframes scrPanelLOnce {
  0% { transform: translateX(0); }
  100% { transform: translateX(-102%); }
}

半分の幅を 50.5% と少し余分に取り、102% まで押し出すのは、小数の丸めで中央や端に実線が1本にじむのを防ぐためです。

中央の縦線が先に倒れる

.scr__seam {
  position: absolute; top: 10%; bottom: 10%; left: 50%; width: 4px; z-index: 3;
  border-radius: $r-pill; background: $stage-orange;
  transform: translateX(-50%) scaleY(0);
  animation-name: scrSeamOnce; animation-duration: $dur-quick;
  animation-timing-function: $easing; animation-iteration-count: 1; animation-fill-mode: backwards;
}
@keyframes scrSeamOnce {
  0% { transform: translateX(-50%) scaleY(1); }
  40%, 100% { transform: translateX(-50%) scaleY(0); }
}

継ぎ目は 220ms の短いアニメーションで、半分が開く前に先に畳んで消えます。開いている間に画面の真ん中に線が残らず、面が大きく動く項目なので累積面積は 78.636% です。

08グリッチ起動

見出しが切り取られた二層にずれ、残像の色が左右へ跳ねたあと、ずれが0へ収束して一行に揃う、ゲームや技術ブランドの入場画面向けイントロです。

グリッチ起動
新しいタブで開く

08 · グリッチ起動 — 計測 累積面積 9.744% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

text-shadowclip-pathanimation-iteration-count

見出しを上下二層に切る

.gl__slice {
  position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none;
  color: $subject-ink;
  font-weight: 800; font-size: clamp(30px, 9vw, 48px); line-height: 1.15; white-space: nowrap;
}
.gl__slice--a {
  clip-path: inset(0 0 50% 0);              // チップ: clip-path — 上半分
  animation-name: glSliceAOnce; animation-duration: $duration;
  animation-timing-function: $easing; animation-iteration-count: 1; animation-fill-mode: backwards;
}
.gl__slice--b {
  clip-path: inset(50% 0 0 0);              // 下半分
  animation-name: glSliceBOnce; animation-duration: $duration;
  animation-timing-function: $easing; animation-iteration-count: 1; animation-fill-mode: backwards;
}

本物の見出しは下にそのまま置き、同じ文字を上下に切った層を2枚重ねて揺らします。読み上げる装置には無傷の一行が残り、目にだけずれが見えます。

ずれが0へ収束する5つのコマ

@keyframes glSliceAOnce {
  0% { transform: translate(-9px, 0); text-shadow: 5px 0 $color; clip-path: inset(0 0 52% 0); }
  7% { transform: translate(5px, -1px); text-shadow: -4px 0 $subject-orange-deep; clip-path: inset(0 0 48% 0); }
  14% { transform: translate(-6px, 1px); text-shadow: 4px 0 $color; clip-path: inset(0 0 55% 0); }
  21% { transform: translate(3px, 0); text-shadow: -3px 0 $subject-orange-deep; clip-path: inset(0 0 47% 0); }
  28% { transform: translate(-2px, 0); text-shadow: 2px 0 $color; clip-path: inset(0 0 50% 0); }
  34%, 100% { transform: translate(0, 0); text-shadow: 0 0 transparent; clip-path: inset(0 0 50% 0); }
}

ずれは 9px から 5・6・3・2px へ縮み、切り取り線も 52・48・55・47% と揺れて 34% で全部定位置に収まります。動くのは文字の面だけなので累積面積は 9.744% と、一文字(03)・線(06)に次ぐ小ささです。

09カラーフラッド

色帯3枚が順に画面を走り抜け、最後の帯が右へ折れて抜けると下の内容だけが残る、色そのものがブランドのサイト向けイントロです。

カラーフラッド
新しいタブで開く

09 · カラーフラッド — 計測 累積面積 79.442% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleXtransform-originanimation-fill-mode

帯は左から伸びて右へ折れる

.cf__band {
  position: absolute; inset: 0; z-index: 2;
  transform-origin: left center;
  transform: translateX(0) scaleX(0);
  will-change: transform;
  animation-timing-function: linear;   // 波の前が一定の速さで通るように — 伸びと折りが同じ曲線でないと継ぎ目が開く
  animation-iteration-count: 1; animation-fill-mode: backwards;
}
@keyframes cfBand1 {
  0% { transform: translateX(0) scaleX(0); }
  13% { transform: translateX(0) scaleX(1); }
  26%, 100% { transform: translateX(100%) scaleX(0); }
}

transform-origin を左に置くと伸びる向きは得られますが、退出まで左へ折れてしまいます。移動と縮小を同じ時間で一緒に補間することで、右端が固定されたように抜けていきます。

3つの帯を13%ずつ並べる

@keyframes cfBand2 {
  0%, 13% { transform: translateX(0) scaleX(0); }
  26% { transform: translateX(0) scaleX(1); }
  39%, 100% { transform: translateX(100%) scaleX(0); }
}
@keyframes cfBand3 {
  0%, 26% { transform: translateX(0) scaleX(0); }
  39% { transform: translateX(0) scaleX(1); }
  52%, 100% { transform: translateX(100%) scaleX(0); }
}
@keyframes cfContentOnce {
  0%, 38% { opacity: 0; transform: translateY(16px); }
  54%, 100% { opacity: 1; transform: translateY(0); }
}

前の帯が折れ始める位置で次の帯が伸び始めるよう 13% ずつずらしてあるので、帯の間に空いた画面が挟まりません。最後の帯が折れ始める 39% の直前に内容が浮かび上がり、累積面積は 79.442% です。

イントロ アニメーション はCSSだけでどこまでできるか?

9つすべてで演出は CSS キーフレームが担い、JavaScript はクラス2つの入れ替えしかしていません。イントロ アニメーション が預かるのは最初の数秒だけなので、覆い一枚・文字一行・色帯数枚で終わる導入なら web オープニング アニメーション css だけで足りて、追加で読み込むファイルはありません。逆に、スクロール位置に縛られた長いシーケンス、粒子が舞うキャンバス、映像と字幕を合わせるタイムラインのように、再生位置をコードで巻き戻す演出は専用ツールの領域です。待つ間の画面が要る場所では軽い待ち画面の作り方が別の答えになります。

どこで壊れるか — 落とし穴

イントロは画面全体を覆う要素を扱うので、普段カードやボタンでは起きない事故が3つの方向で起きます。

全面要素に scale を掛けるとレスポンシブ検査が壊れる

// 縦の束 — dolly-out は画面より小さいこの箱に掛ける(全面要素を scale up すると scrollWidth が溢れる)
.iw__box {
  display: flex; flex-direction: column; align-items: center;
  width: min(84%, 400px); gap: $sp-2; text-align: center;
}
@keyframes iwSettle {
  0% { transform: scale(1.045); }
  78% { transform: scale(1); }
  100% { transform: scale(1); }
}

04 の初版は、画面を満たした inset: 0 の要素に scale(1.045) を掛けていました。overflow: hidden を付けても scrollWidth は変換後の境界箱で計算されるので、拡大したぶん横に溢れ、レスポンシブ検査では狭い幅で、はみ出しとして掴まりました。拡大は画面より小さい内側の箱 .iw__box へ移し、05 の浮上も同じ理由で .cd__box の内側にだけ掛けて、どちらもはみ出し判定は false のままです。

折り返しが登場より速いと代表画像が空の画面になる

@keyframes clTopLoop {
  0%, 4% { transform: translateY(0); }
  26%, 66% { transform: translateY(-102%); animation-timing-function: linear; }
  88%, 100% { transform: translateY(0); }
}
@keyframes scrPanelLLoop {
  0%, 6% { transform: translateX(0); }
  45% { transform: translateX(-102%); }
  70% { transform: translateX(-102%); animation-timing-function: linear; }
  90%, 100% { transform: translateX(0); }
}

グリッドに掛かる代表画像は、24枚を隣同士で比べた23区間のうち、変わった面積がいちばん大きい瞬間として選ばれます。イントロのループは開いた後にまた閉じなければならず、折り返しを短く切るとその1区間が最大変化になって、代表画像が幕に覆われた空の画面で撮られます。そこで 01 と 07 は開く側にスプリングを置き、閉じ直す区間だけ中間キーフレームの animation-timing-function: linear で長く伸ばし、消える場面も不透明度の代わりに覆いが再び被さる形にしました。9つを一度に受け取る圧縮ファイルのパスワードは g2nru27q で、02 のように欠片が散らばる項目も同じ理由から、散らばりの区間をコマではなく線形にしています。

コマ送りは区間の終わりでだけ値を変える

// steps(1, end) は区間の終わりでだけ値が変わる — コマ位置(14・28・42%)を中間キーフレームで直接置く
.cd.is-demo .cd__site {
  animation-name: cdSite;
  animation-duration: $duration;
  animation-timing-function: steps(1, end);
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}
@keyframes cdSite { 0% { opacity: 0; } 42% { opacity: 1; } 100% { opacity: 1; } }

steps(1, end) はひとつの区間を1コマと見て、その終わりでだけ値を変えます。0% と 100% にだけ値を置くと、切り替わる瞬間が再生の終わりに寄ってしまって、画面では何も起きません。05 のカウントダウンは数字の変わる 14・28・42% を中間キーフレームとして直接置いて3つのコマを作り、06 の文字の点灯も 0% と 2% の間にコマ位置を置いて同じ落とし穴を避けました。

3つのバリエーションとアクセシビリティ

トークン3つを書き換えるだけで、9つの性格が一緒に変わります。

値3つで9つを一緒に回す

名前 書き換える値 変わる感じ
duration $dur-enter 600ms → 300ms 幕と文字が倍の速さで片付く
easing $ease-spring → $ease-pop 見出しが収まるときもう一度跳ねる
color $subject-blue → $stage-orange 覆いの色が寒色から暖色へ

動きを減らしたときに残る一枚

9つすべての基本状態が完了画面なので、prefers-reduced-motion でアニメーションを切るだけで、イントロの終わった画面がそのまま残ります。01〜06 は animation: none !important、07〜09 は animation-name: none !important で、プレビュー規則より優先させます。巻き戻すべき状態がないので、動きを断った人は覆いもカウントダウンも見ずに本文へ着きます。

目と耳に同じものが残るように

幕・欠片・色帯のような飾りの要素には aria-hidden="true" を付け、スキップした事実は aria-live="polite" の一行が知らせます。9つの画面の文字のコントラストをトークンの値から直接計算して AA 未達は0件で、01 の見出しはインク背景 #17141a の上で 17.11:1、03 の一文字はオレンジ #ff4d1f の上で 5.50:1、09 の上部のラベルはインク #17141a の上で 5.69:1 です。手法の根拠は clip-path・stroke-dasharray・sessionStorage の文書にあり、画面が変わっている間の処理の続きはページ遷移 ローディング 9選が、軽い待ち画面はCSS ローディング スピナー 9選が扱います。

FAQ

更新のたびにイントロをまた見せても良いですか

9つすべて sessionStorage に印をひとつ残すので、同じタブで戻ってくればイントロを飛ばします。実測では、スキップの直後に印が 1 と書き込まれ、再訪では読み込みの直後に完了状態で始まって、スキップボタンも隠れます。タブを閉じると印も消えるので、初めての人には1度だけ見えます。

html css オープニング アニメーション を入れると最初の画面は遅く見えませんか

9つは画像も外部ライブラリも追加で読まず、CSS キーフレームとわずかなスクリプトだけを使います。内容は覆いの下にすでに不透明で敷かれていて、後から作られるのではなく隠れているだけです。それでも短くしたいなら、再生時間を 300ms に縮める duration のバリエーションがいちばん手数が少ないです。

スクリーンリーダーではイントロはどう聞こえますか

覆いと欠片は全部 aria-hidden なので読み上げられず、03 のように文字を割った項目は行全体に aria-label を付けて、一文字ずつではなくひとつの単語として聞こえます。スキップボタンは最初からタブ順序の中にあり、Esc も同じ関数を呼ぶので、マウスがなくても最初のキー入力で本文へ進めます。

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

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