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

css crt effect 9選 — レトロOS UI をコピペで

css crt effect は、Windows 98 の画面を HTML・SCSS・JS だけで組み直すレトロ UI の定番です。ここに並べた9つの部品はあの頃の机の上をそのまま再現していて、ウィンドウは本当に動いて最小化します。

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

9つの並び順は、昔のパソコンの一日の流れです。最初の3つは電源を入れる前の部品で、手でつかむウィンドウ(01)、目で見るブラウン管(02)、画面の中を動き回るカーソル(03)です。電源が入ると点検の行が流れ始め(04)、デスクトップのアイコン(05)、スタートメニュー(06)、タスクバーの時計(07)が順に息を吹き返し、インストールを待つ時間(08)を経て、青いエラー画面と再起動(09)で一日が閉じます。舞台の色もこの流れに合わせて、インク・イエロー・オレンジ・ペーパーが2周してインクで終わります。手触りをさらに強めたい方向けの姉妹編がスキューモーフィックなコントロールボタン9選で、同じ時代の派手な側がY2Kクローム・ホログラムのランディングです。04 がタイピングカーソルと ASCII の進捗バーをあえて使っていないのはターミナル UI 9選とCSS タイピング演出9選と重ならないためで、文字がひと字ずつ打たれる側が必要ならその2編をご覧ください。

01ドラッグできるウィンドウと最小化

タイトルバーをつかむとウィンドウが指先に付いてきます

タイトルバーをつかんで引くと、指がウィンドウの外へ出ても setPointerCapture がポインタを離さず、pointermove が届くたびに translate でウィンドウを動かします(MDN の Pointer Events)。動かせる範囲は領域とウィンドウの幅の差の半分までに制限してあって画面の外へは出ないので、ウィンドウをいくつも重ねて見せる紹介画面に載せれば、スライドではなく触れる板になります。

ドラッグできるウィンドウと最小化
新しいタブで開く

01 · ドラッグできるウィンドウと最小化 — 計測 累積面積 28.026% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setPointerCapturebox-shadowtranslate
// 畳む/展開 — 畳むとき枠の中心へ translate を移し scale を縮める。元の位置は saved に置く
function setMin(on) {
  min = on;
  win.classList.toggle('is-min', on);
  slot.classList.toggle('is-min', on);
  win.classList.add('is-flying');
  if (on) {
    savedX = px; savedY = py;
    var wr = win.getBoundingClientRect(), sr = slot.getBoundingClientRect();
    px += (sr.left + sr.width / 2) - (wr.left + wr.width / 2);
    py += (sr.top + sr.height / 2) - (wr.top + wr.height / 2);
  } else {
    px = savedX; py = savedY;
  }
  place();
  window.setTimeout(function () { win.classList.remove('is-flying'); }, 320);
}

畳む瞬間に枠の中心を計算し直す理由

最小化を押すと、ウィンドウの中心とタスクバーの枠の中心の差をその瞬間に計算し直し、その枠へ scale .12 で吸い込まれるように畳まれて、枠をもう一度押すと保存しておいた位置へ戻ります。外枠は明るい面と暗い面を4枚重ねた box-shadow で盛り上がって見えます。ボタン単体なら明暗1組で足り、これが windows 98 button css の基本的な凹凸の作り方です。

02CRT 走査線と曲面ビネット

縞タイル1枚と楕円の影1枚

横縞は repeating-linear-gradient のタイル1枚(2px の線 + 2px の透過)がすべてで、background-size 100% 4px で画面全体に繰り返されます。その上に、端へ行くほど暗くなる楕円の radial-gradient を重ねてブラウン管の曲面ガラスを真似ていて、明るい帯が1本、上から下へ流れます。帯の始点と終点はどちらも画面の外なので、ループが閉じる継ぎ目は見えません。

CRT 走査線と曲面ビネット
新しいタブで開く

02 · CRT 走査線と曲面ビネット — 計測 累積面積 37.75% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

repeating-linear-gradientradial-gradientbackground-size
// ── 走査線 — タイル(4px)を background-size で定義し、その中に線の位置を置く
.crt__lines {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background-image: repeating-linear-gradient(to bottom,
    rgba($subject-ink, .38) 0 2px,
    transparent 2px 4px);
  background-size: 100% 4px;
}

// ── 曲面ビネット — 楕円で縁が内へ巻き込まれるガラス
.crt__vig {
  position: absolute; inset: 0; z-index: 4; pointer-events: none;
  border-radius: inherit;
  background: radial-gradient(ellipse 130% 105% at 50% 46%,
    transparent 40%,
    rgba($subject-ink, .18) 64%,
    rgba($subject-ink, .55) 100%);
}

一番広く動いて一番静かな面

計測値は累積面積 37.75% で9つの中で最も広い一方、強度の平均は 16.3 と最も静かです。レトロな色調を敷きたいヒーロー背景に使っても、手前に置いた文字を邪魔しません。

03ピクセルカーソル3種

box-shadow で描くドット絵

矢印・砂時計・指差しの3つのカーソルがチェッカーボードの格子の上で steps(1, end) のカットで順に入れ替わり、砂時計はその場で180度反転します。絵は pxRuns 関数が横一行の座標(x y 幅)を box-shadow の影へ展開したドット絵で、拡大は整数倍(scale 6)だけに限り、image-rendering に pixelated を指定してあるので、ピクセルカーソルは大きくしても輪郭がにじみません。

ピクセルカーソル3種
新しいタブで開く

03 · ピクセルカーソル3種 — 計測 累積面積 9.179% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

image-renderingconic-gradientsteps(1, end)
// ── pixel の絵 — 横一行の runs (x y 幅)。K は黒の輪郭、W は白の塗り、S は砂
@function pxRuns($runs, $c) {
  $s: ();
  @each $r in $runs {
    @for $i from 0 through list.nth($r, 3) - 1 {
      $s: list.append($s, ((list.nth($r, 1) + $i) * 1px) (list.nth($r, 2) * 1px) 0 0 $c, comma);
    }
  }
  @return $s;
}

一番細かく動く入れ替え

ボタンごとに実寸の2倍のプレビューを添えてあり、動いたフレームは 22/24 で9つの中で最も細かく動きます。カーソルまで差し替えるテーマページにそのまま使えます。

04起動チェック行の出力

行がひとつずつ積まれ、結果が一拍遅れて付く

MS-DOS の画面を思わせるデバイス点検の行が5つ、clip-path を右へ開く動きで一行ずつ下に積まれ、各行の右端には結果の文字が一拍遅れてカットで現れた後、8px 横から定位置へ滑り込みます。最後の行まで埋まると画面が一度消え、最初から数え直します。

起動チェック行の出力
新しいタブで開く

04 · 起動チェック行の出力 — 計測 累積面積 9.339% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

animation-fill-modeclip-pathmonospace
@keyframes blLogo {
  0% { clip-path: inset(0 100% 0 0); }
  6%, 8% { clip-path: inset(0 0 0 0); }
  14%, 75% { clip-path: inset(0 0 0 100%); }
  76%, 88% { clip-path: inset(0 0 0 0); }
  90%, 100% { clip-path: inset(0 100% 0 0); }
}
@keyframes blRow1 {
  0%, 12% { clip-path: inset(0 100% 0 0); }
  17%, 75% { clip-path: inset(0 0 0 0); }
  76%, 100% { clip-path: inset(0 100% 0 0); }
}

点検の行がそのままローディングの文言になる面

画面をクリックしても Enter・Space キーでも、いつでも最初から数え直せます。待ち時間を物語で埋めるローディング画面にそのまま載せられます。

05ダブルクリックで開くデスクトップアイコン

一度押せば選択、短い間に二度押せば開く

一度押すとラベルだけが選択色に変わり、短い間に二度押すとアイコンが自分の中心で一度大きくなってウィンドウが開きます。ダブルクリックの判定は click イベントの e.detail(同じ場所で続けて押した回数)で行い、タッチではこの値が常に1なので、400ms 以内に同じアイコンをもう一度押したかどうかを判定の補助にします。

ダブルクリックで開くデスクトップアイコン
新しいタブで開く

05 · ダブルクリックで開くデスクトップアイコン — 計測 累積面積 12.865% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

detailaria-selectedtransform-origin
icons.forEach(function (b, i) {
  b.addEventListener('click', function (e) {
    dt.classList.remove('is-demo');
    select(i, false);
    // e.detail は同じ場所で続けて押した回数 — 2 以上ならダブルクリック
    // タッチでは detail が常に1なので時間差(400ms)で判定する
    if (e.detail >= 2 || (i === lastIdx && Date.now() - lastTap < 400)) openWin(i);
    lastTap = Date.now();
    lastIdx = i;
  });
});

方向キーでも歩けるアイコンの格子

方向キーで選択が移り、選択状態は aria-selected で読み上げにも伝わります。項目をアイコンの格子から選ぶ画面にそのまま使えます。

06スタートメニューの展開

下から上へ開き、横へ続いて開くメニュー

左に縦帯(writing-mode の縦書き)の付いたメニューがタスクバーの上で下辺を支点に開き、矢印のある行にポインタを重ねると下位メニューが右へ続いて開きます。下位メニューを開いた後に getBoundingClientRect で実測し、右端が画面の外へ出るようなら左へ折り返し、折り返すと左が切れる場合は元の向きのままにします。

スタートメニューの展開
新しいタブで開く

06 · スタートメニューの展開 — 計測 累積面積 11.544% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-expandedwriting-modetransform-origin
// 下位メニューを展開した後に実測し、右が画面の外へ出るなら左へ折り返す。
// 左まで切れるなら元の向きを守る(両側を満たせない幅はないとみなす)
function showSub(sub) {
  hideSubs();
  sub.classList.add('is-on');
  var r = sub.getBoundingClientRect();
  if (r.right > window.innerWidth - 8) {
    sub.classList.add('is-flip');
    if (sub.getBoundingClientRect().left < 8) sub.classList.remove('is-flip');
  }
}

折り返す向きまで自分で選ぶ構造

下位メニューが画面の外へはみ出さないよう、開く向きを自分で選びます。項目の多い管理画面のメニューに載せても、狭いウィンドウで崩れません。

07タスクバーの時計とトレイ

実際の時刻を分単位で数える時計

時計は toLocaleTimeString でページの言語に合わせて実際の時刻を分単位で表示し、5秒ごとに確かめ直します。ウィンドウボタンは押された状態と浮いた状態で border-color の明るい面と暗い面が互いに逆向きへ入れ替わり、押すと translateY(1px) で1px 沈むので、画像がなくても凹凸が伝わります。

タスクバーの時計とトレイ
新しいタブで開く

07 · タスクバーの時計とトレイ — 計測 累積面積 5.797% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

toLocaleTimeStringinsetborder-color
// 時計 — ページの言語に従う toLocaleTimeString で実際の時刻を分単位で打つ
function tick() {
  var lang = document.documentElement.lang || 'ko';
  var text = new Date().toLocaleTimeString(lang, { hour: 'numeric', minute: '2-digit' });
  if (clockReal.textContent !== text) clockReal.textContent = text;
}
tick();
setInterval(tick, 5000);

兄弟セレクタ1つで浮かぶ吹き出し

通知のベルが点くと吹き出しがトレイの上へ浮かぶところまで CSS の兄弟セレクタ1つでできているので、常に状態を見せる下部バーにそのまま貼れます。ウィンドウボタンを縁の4か所の色の入れ替えだけで凹ませる作りは windows 98 taskbar css を組むときの定番の形で、針の角度まで数える時計はJavaScript のアナログ時計9選が受け持ちます。

08インストール進捗バーと段階の文言

滑らかに伸びない12のマス

進捗バーは、linear-gradient 1つを 80% のマス + 20% のすき間の比率で定義して background-size 20px のタイルとして敷いたマスの型の上で、トラック色の覆いを steps(12, end) で縮めながら四角いマスを左からひとつずつ現します。マスの拍に合わせて下の文言5枚と残り容量の数字12枚がカットで差し替わり、埋まり切ると完了の印が一度跳ねます。

インストール進捗バーと段階の文言
新しいタブで開く

08 · インストール進捗バーと段階の文言 — 計測 累積面積 2.866% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

steps(12, end)linear-gradientbackground-size
.ip__fill {
  position: absolute; inset: 0;
  background-image: linear-gradient(90deg, $color 0 80%, transparent 80% 100%);
  background-size: $tile 100%;
  background-repeat: repeat-x;
}
// トラック色の覆い — 右に張り付いて scaleX で縮み、マスを左から現す。
// 埋まり始める辺に付くため transform-origin を右の中央に置く(中心ではない理由)
.ip__cover {
  position: absolute; inset: 0;
  background: $stage-paper;
  transform-origin: right center;
  transform: scaleX(0);
}

キーフレーム1つを17枚が分け合う負の遅延

文言も数字も、同じキーフレームを負の animation-delay で位相だけずらして使い回します。この組み方なので、時間のかかる処理の待機画面に載せても拍がずれません。

09青いエラー画面と再起動

1フレームでまとめて現れる青いエラー

エラーの文言はフェードなしの steps(1, end) のカットで1フレームにまとめて現れ、案内の行の点3つが順に点いたまま残ります。再起動を押すと clip-path のワイプで画面が上から下へ拭き取られ、元のウィンドウが現れて transitionend の後に状態が片付きます。

青いエラー画面と再起動
新しいタブで開く

09 · 青いエラー画面と再起動 — 計測 累積面積 36.953% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

steps(1, end)clip-pathanimation-delay
// 登場のカット — 中間キーフレーム(7%)を置かないと8% の地点で1フレームに変わらない
@keyframes bsCut {
  0%, 7% { opacity: 0; }
  8%, 100% { opacity: 1; }
}
// 退場のワイプ — steps ではなくイージングで補間される対のアニメーション
@keyframes bsWipe {
  0%, 58% { clip-path: inset(0 0 0 0); }
  70%, 100% { clip-path: inset(100% 0 0 0); }
}

計測でも最も重い面

累積面積 36.953% と面積の最大 36.953% が同じ数字なのは、動きのすべてが、青い面がウィンドウを覆う一度の切り替えから来ているからです。致命的なエラーを大きく知らせる画面としては、9つの中で最も重い部類になります。

crt scanline effect css はCSSだけでどこまでできるか?

縞はタイル1枚がすべてです

走査線の太さ・間隔・色は4px のタイル1枚がすべて決めていて、background-size でタイルの大きさを指定しておけば、画面がどれだけ大きくなっても模様が伸びずに本数だけが増えます。08 のマスのバーの型も同じ理屈で、グラデーションのカラーストップは px ではなくタイルの中の比率で与えます。px で与えるとタイルではなく要素全体の幅を一度に敷くグラデーションになって、マス模様が消えます。

曲面ガラスは暗さの重ねで畳みます

膨らんだガラスは、実際には画面を歪めません。radial-gradient の楕円で縁を暗く敷けば目が勝手に曲面だと解釈しますし(02)、蛍光体のにじみは画像ではなく text-shadow 1行、上から降りてくる逆光は半透明の帯の縦の流れで真似ました。本物のブラウン管の樽形歪みのように画面の端が曲がって見える幾何学的な歪みまで欲しければ、CSS だけでは足りず SVG フィルタか canvas が必要になります。この9つは、その手前の質感までを狙っています。

画像ファイルは1枚もありません

9つを通して画像ファイルは0枚です。ピクセルカーソル(03)は box-shadow のドット絵、チェッカーボードの格子は conic-gradient、モニターの筐体は色面と影なので、絵が必要な場所をすべて値に置き換えました。受け取ったフォルダの中はコードだけで、ドット絵は倍率を変えても描き直されるのでにじみません。画面の背景の濃紺(#0d1220)だけがトークンの外の色で、ブラウン管の蛍光面の地は既存のパレットにない色なので、ここ1か所だけ直接指定しました。

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

キーフレーム2枚だけのカットは見えません

steps(1, end) のカットに 0%{非表示} 100%{表示} の2枚だけを入れると、値が区間の最後でのみ変わるため、2秒間ずっと非表示の面だけを見ることになります。カットはループが閉じるその瞬間に起きて画面の中では見えないので、09 の青い画面はカット位置を 7%・8% の中間キーフレームの組に置いて初めて1フレームで現れました。状態が2つだけのほかの面も同じ理由で中間キーフレームを置いています。01 のタスクバー枠は 50%・51%・85%・86% の地点に、07 の通知ベルは 15%・16%・79%・80% にカットを置きました。

短くて便利な一括指定が遅延を消します

animation の一括指定を使った瞬間、animation-delay は0へ戻ります。行ごとに拍の違う 04 のような面で一括指定に書き直すと遅延が消えて5行が一度に流れてくるので、この9つで遅延を使う場所(08 の数字12枚、09 の点3つ)は名前・長さ・タイミングをすべて個別のプロパティで書きます。この9つの完成版と React 版をまとめた zip は sqx22eqz をパスワードにして展開でき、エディタがなくてもブラウザで開いてすぐ試せます。

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

変数3つで9つをまとめて変える

各デモの SCSS の先頭にある変数をいくつか変えるだけで、舞台全体が付いてきます。変数はデモごとに1組なので、ファイルを1つだけ触り、残り8つはそのまま残して見比べられます。

名前 変える値 感じ
duration $dur-loop 2s → 1s 起動の行とインストールのマスの拍が2倍速くなります
easing $ease-spring → $ease-pop ウィンドウが畳まれるときとエラー画面が拭われるときに、もう一度跳ねます
color $color $subject-blue-deep → $subject-orange-deep 選択色とタイトルバーが青系から橙系に変わります

動きを減らしたときに残るもの

prefers-reduced-motion: reduce では、9つがそれぞれ違う1枚を残します。01 は畳まれていないウィンドウを1枚、02 は縞とビネット(帯だけ止まります)、03 は矢印カーソル、04 は点検の行が埋まった画面、05 は最初のアイコンが選ばれてウィンドウが開いた姿、06 はメニューが開いたまま下位を隠した姿、07 は実際の時刻を示す時計、08 はバーが埋まった完了状態、09 はエラー画面を消した元のウィンドウです。そのブロックの宣言に1つずつ !important を付けてあるのは、.is-demo の付いたセレクタのほうが詳細度で勝つためです。07 の時計はループを止めても tick が実際の時刻を更新し続けるので、止まった画面が古い時刻を示すことがありません。

キーボードと色以外の信号

マウスがなくても使えます。01 のタイトルバーは方向キーで動かせて(Shift を押しながらなら16px)、Escape で畳めます。05 は listbox の中で方向キーと Enter、06 は方向キーで行を渡って Escape で閉じ、09 はエラー画面が現れた瞬間に再起動ボタンへフォーカスを移します。色だけに頼ってもいません。07 の押された状態は縁取りの向きの反転に文字の太さと1px の沈み込みが重なり、05 の選択は aria-selected とラベルの背景が同時に変わり、03 はカーソルの形の横にラベルを添えてあります。

FAQ

crt text effect css のように文字だけを蛍光させたい場合は何を残しますか

文字への text-shadow 1行と、文字の上に重ねる縞のタイルだけを残します。02 の4枚の層のうち帯とビネットを外せば、画面全体ではなく文字だけを光らせる部品になります。にじみが強すぎると感じたら、まず影の色の不透明度を下げてください。縞が濃いと感じたら、走査線の1色目の不透明度 .38 を下げると、目への負担がすぐに減ります。

モバイルでもウィンドウのドラッグとダブルクリックは動きますか

どちらも動きます。01 のタイトルバーには touch-action: none を指定し、Pointer Events でドラッグを受け取るので、指がウィンドウの外へ出ても離しません。05 のダブルクリックは、タッチでは e.detail が常に1のため、400ms 以内に同じアイコンをもう一度押したかどうかで判定します。340px より狭い画面では各デモが自分で縮んで収まり、05 は縦に並んでいたアイコンが横並びに変わります。

ピクセルフォントを別にダウンロードする必要はありますか

いいえ。9つともシステムの等幅フォントのスタック(ui-monospace 系)1つで組んであり、Web フォントは1つも使いません。ピクセル感が要る場所(03 のカーソル)はフォントではなく box-shadow のドット絵なので拡大してもにじまず、システムフォントなので最初の描画までの待ちも短く、待機画面に向いています。等幅フォントを選んだのは、点検の行・残り容量・時計のように桁がずれてはいけない文字が多いためです。プロポーショナルフォントに変えると数字が揺れて、昔の画面の整った感じが消えます。

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

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