アカウント セキュリティ 設定 UI 9選 — ログイン後の画面
アカウント セキュリティ 設定とは、ログインを済ませた人が次に巡り会う画面のことです。今つながっている端末、二段階認証、パスワードそのものを、本人の手で確かめて変えていく場所です。9つすべて実際に動きます。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 いまログイン中の端末一覧
- 02 二段階認証を入れる三つの手順
- 03 一度使うと線が引かれるバックアップコード
- 04 指紋から盾へ切り替わるパスキー登録
- 05 三つの欄がそろって開くパスワード変更
- 06 新しい端末のログイン通知スイッチ
- 07 一度だけ見せるアプリパスワード
- 08 成功と失敗が一緒に並ぶログイン履歴
- 09 文字を打ち込んで初めて開く退会
並び順は、セキュリティの画面を初めて開いた人が疑問を持つ順です。まず誰が入っているかを見ます(01)。次に二段階認証を一歩ずつ進め(02)、バックアップコードを受け取り(03)、パスワードなしで入れるようこの端末を登録します(04)。ここからは変える側です。パスワードを替え(05)、見知らぬ端末の通知をどこで受けるか決め(06)、メールアプリに渡すアプリパスワードを発行します(07)。最後の2つは見張る側です。過去のログインを眺めて怪しい接続を探し(08)、取り消せない削除の前に一度だけ言葉を置きます(09)。
01いまログイン中の端末一覧
端末アイコンと接続した都市、最後に使った時刻が一行に並び、いま見ている端末には別のバッジが付きます。行の右のボタンを押すとその行だけ横へ滑り出し、高さも一緒に畳まれるので下の行が跳ねません。
.sd.is-demo .sd__row[data-row="3"] {
animation-name: sd-out;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes sd-out {
0% { transform: translateX(0); max-height: var(--row-h); background-color: transparent; }
28% { transform: translateX(0); max-height: var(--row-h); background-color: transparent; }
31% { transform: translateX(0); max-height: var(--row-h); background-color: $error-soft; }
48% { transform: translateX(112%); max-height: var(--row-h); background-color: $error-soft; }
58% { transform: translateX(112%); max-height: 0; background-color: $error-soft; }
68% { transform: translateX(112%); max-height: var(--row-h); background-color: $error-soft; }
84% { transform: translateX(0); max-height: var(--row-h); background-color: $error-soft; }
87% { transform: translateX(0); max-height: var(--row-h); background-color: transparent; }
100% { transform: translateX(0); max-height: var(--row-h); background-color: transparent; }
}
02二段階認証を入れる三つの手順
アプリでコードを読む場面、六桁を入れ直す場面、バックアップコードを受け取る場面が一枚のカードの中で順番に開きます。手順ごとに上の目盛りが一つ埋まり、通り過ぎた手順は開き直しません。
.tf.is-demo .tf__panel {
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: steps(1, end), $easing;
animation-iteration-count: infinite, infinite;
}
.tf.is-demo .tf__panel[data-panel="1"] { animation-name: tf-cut1, tf-rise1; }
.tf.is-demo .tf__panel[data-panel="2"] { animation-name: tf-cut2, tf-rise2; }
.tf.is-demo .tf__panel[data-panel="3"] { animation-name: tf-cut3, tf-rise3; }
@keyframes tf-cut1 { 0% { opacity: 1; } 30% { opacity: 0; } 86% { opacity: 1; } 100% { opacity: 1; } }
@keyframes tf-cut2 { 0% { opacity: 0; } 30% { opacity: 1; } 60% { opacity: 0; } 100% { opacity: 0; } }
@keyframes tf-cut3 { 0% { opacity: 0; } 60% { opacity: 1; } 86% { opacity: 0; } 100% { opacity: 0; } }
03一度使うと線が引かれるバックアップコード
九文字のコードが六つ、カードの中に二列のグリッドで並び、すでに使ったコードには真ん中に線が引かれて薄く残ります。下のボタンは一覧全体をクリップボードへ渡し、その瞬間を読み上げ領域にも一度流します。
btn.addEventListener('click', function () {
root.classList.remove('is-demo'); // 사람이 만졌으니 자동 루프를 끈다
clearTimeout(t);
root.setAttribute('data-copied', '1');
var txt = 코드전체();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(txt);
}
live.textContent = '백업 코드 6개를 클립보드에 복사했습니다';
t = setTimeout(function () { root.setAttribute('data-copied', '0'); }, 1600);
});
04指紋から盾へ切り替わるパスキー登録
中央の指紋が膨らんでは戻るのを繰り返し、端末が指を待っていることを示します。登録が終わると盾の印へ一フレームで切り替わり、待っている間ボタンは固まり、その事実は属性にも書かれます。
// 호는 기다리는 구간에만 보인다 — 회전과 컷을 한 요소에 겹쳐 건다
.pk.is-demo .pk__arc {
animation-name: pk-spin, pk-busy;
animation-duration: 900ms, $dur-loop;
animation-timing-function: linear, steps(1, end);
animation-iteration-count: infinite, infinite;
}
// 지문은 기다리는 동안 부풀었다 줄기를 반복한다
.pk.is-demo .pk__ic[data-ic="print"] {
animation-name: pk-print, pk-pulse;
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: steps(1, end), $ease-spring;
animation-iteration-count: infinite, infinite;
}
05三つの欄がそろって開くパスワード変更
いまのパスワード、新しいパスワード、確認欄がすべて条件を満たすまで保存は開きません。新しい欄の下の四つの目盛りはグリッドで切ってあるので幅がずれず、確認欄が違うとその行だけ赤く残り、理由が添えられます。
// ── 네 칸 강도 눈금 — 칸 폭은 grid 가 나눈다 (scaleX 는 칸 안쪽 채우기에만) ──
.pw__meter {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $sp-1;
margin-top: $sp-1;
}
.pw__cell {
display: block;
overflow: hidden;
height: 6px;
border-radius: $r-pill;
background-color: rgba($subject-ink, .14);
}
06新しい端末のログイン通知スイッチ
メール、プッシュ、SMSの三行がスイッチを一つずつ持ち、つまみが左右に滑ります。一つ変えると下に保存済みの行が灯り、その行は消えずに次の変更まで残るので、代表画像が空の画面になりません。
// 실제 동작: role="switch" 인 <button> 이라 Space·Enter 가 그대로 click 으로 온다 — 따로 가로채지 않는다
var root = document.querySelector('.al');
var 스위치 = root.querySelectorAll('.al__sw');
Array.prototype.forEach.call(스위치, function (sw) {
sw.addEventListener('click', function () {
root.classList.remove('is-demo'); // 사람이 만졌으니 자동 루프를 끈다
var on = sw.getAttribute('aria-checked') !== 'true';
sw.setAttribute('aria-checked', String(on));
root.setAttribute('data-saved', 'true'); // 저장 줄은 사라지지 않고 다음 변경까지 남는다
});
});
07一度だけ見せるアプリパスワード
発行を押すと隠されていた場所が左から一コマずつ剥がれて文字が現れ、コピーが済むと同じ場所が点で覆われて二度と開きません。値が一度しか見えないという注意は、現れる前からカードに書かれています。
// ── 자동 루프: 덮개가 16칸으로 끊어지며 걷히고, 드러난 자리는 그대로 둔다 (퇴장을 재현하지 않는다) ──
.tk.is-demo .tk__cover {
animation-name: tk-strip;
animation-duration: $dur-loop;
animation-timing-function: steps(16, end);
animation-iteration-count: infinite;
}
@keyframes tk-strip {
0% { width: 100%; }
10% { width: 100%; }
56% { width: 0%; }
100% { width: 0%; }
}
08成功と失敗が一緒に並ぶログイン履歴
縦の線に沿って時刻と場所が積み上がり、点の色で成功と失敗が分かれます。いつもと違う国から入った行には枠がもう一重付き、その行だけゆっくり呼吸するので最初に目に留まります。
// ── 자동 루프: 줄들이 translateY 시차로 하나씩 떠오른다. 단축 금지 · 지연에는 backwards 필수 ──
// 떠오름(transform)과 켜짐(opacity)을 갈라 건다 — opacity 를 같이 보간하면 반투명 유령 프레임이 생긴다
.lh.is-demo .lh__item {
animation-name: lh-rise, lh-show;
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: $easing, steps(1, end);
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.lh.is-demo .lh__item[data-i="1"] { animation-delay: 40ms, 40ms; }
.lh.is-demo .lh__item[data-i="2"] { animation-delay: 200ms, 200ms; }
.lh.is-demo .lh__item[data-i="4"] { animation-delay: 360ms, 360ms; }
.lh.is-demo .lh__item[data-i="3"] { animation-delay: 520ms, 520ms; }
// 한 번 올라온 줄은 그대로 머문다 (퇴장을 재현하지 않는다). 22% × 2s = 440ms 만에 선다
@keyframes lh-rise {
0% { transform: translateY(16px); }
22% { transform: translateY(0); }
100% { transform: translateY(0); }
}
09文字を打ち込んで初めて開く退会
赤い区画の入力欄に決められた語をそのまま打ち込むまで削除は固まったままで、文字が合った瞬間にだけボタンが開きます。押して出る確認ダイアログは警告として開くので、フォーカスが先にその中へ入ります。
창.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { 열기(false); return; }
if (e.key !== 'Tab') return;
e.preventDefault(); // 초점이 확인창 밖으로 못 나간다
(document.activeElement === 아니오 ? 예 : 아니오).focus({ preventScroll: true });
});
どこで壊れるか — 落とし穴
状態をopacityでなめらかにつなぐと、24フレームのプレビューは二つの画面が半透明で重なった幽霊を一枚撮ります。02の三つの手順パネルは最初フェードで書きましたが、いちばん大きく変わるフレームがQRコードと六桁の欄と完了の盾を三枚重ねで写した一枚だったのに気づいて切り替えました。画面の交換は steps(1, end) で一フレームに断ち、その横に translateY のポップと scaleX の目盛り埋めを並べて、動くフレーム数を確保しました。この形の02は24枠のうち22枠に動きを記録しています。08の行の浮上も同じ切り分けで、移動はスプリングのイージングに、点灯は一フレームのカットに乗せています。
狭い画面には規則を決めてあります。幅320pxではステージの高さが200pxで、9つの部品の高さを測ると116px(07)から172px(05)の間でした。三つの欄に目盛りと注意文が全部入る05がいちばん高く、余白は28pxでした。そこで狭い画面では値を増やさず減らします。06はスイッチのトラックが40×22pxから32×18pxへ縮みますが、キーフレーム内のつまみの移動距離18pxはメディアクエリの外にある値なので、そのままだと2pxはみ出します。そこで狭い幅のブロックの中で al-knob-on・al-knob-push・al-knob-sms の三つのキーフレームを、移動距離14pxで丸ごと定義し直しました。
測ってみると、9つは性格で分かれます。09は面積38.4%に強度160.7で、いちばん広く、いちばんくっきり動きます。確認ダイアログがスクリーンごと一フレームで開く場面が、それだけの画面を一度に塗り替えるからです。逆に03は面積8.3%しかないのに、強度は152.6で二番目に高い。打ち消し線は細い棒なので面積は小さいのですが、明るいカードの上の濃い赤い線は差が大きいのです。08は四つの行が時差で浮上し、50%の地点で三つの行が同時に暗くなる場面が最大の変化となって36.6%を記録しました。一行が浮くより三行が一度に変わる方が面積が大きくなる、いつもの規則をこの記事でもそのまま使っています。07の覆いは16段のstepsで剥がれます。段の間隔は、タイミング関数が当たる「幅が落ちる区間」10%から56%の920msを16等分した57.5msです。その間に発行ボタンが押される反発を横に添えて、動くフレーム17枠を埋めました。
08を作りながら、遅延の扱いについて規則を一つまとめました。四つの行は40ms・200ms・360ms・520msと時差で浮上しますが、この遅延は animation-delay で与えつつ、必ず animation-fill-mode: backwards と組にします。fill-modeがないと遅延の間、要素は書いたままの姿で描かれるので、0フレームに完成した一覧が一瞬現れて消える瞬きが撮れます。順番もDOMの順とわざと変えました。見慣れない国の行はDOMでは三番目ですが、遅延は520msでいちばん遅く浮上します。最後に届く行こそ、目の止まる行だからです。さらに05の四つの目盛りは animation-delay を使わず、目盛りごとに自分の時間割を持つキーフレーム四つで組みました。共通の規則に animation: の一括指定を置くとdelayが0に戻り、240番で五つの枠が同時に動いた落とし穴を踏み直すことになります。
04は回る弧一本で測定を稼ぎました。状態の交換を steps(1, end) のカットだけで組むとフレームが二枚しか残らず、レンダーの測定を通れません。そこで待っている区間だけ900ms周期で回る弧を同じ要素に重ね、カットは指紋から盾へ切り替わる瞬間だけに使いました。アニメーション名をカンマで二つ並べ、タイミング関数は弧が linear, steps(1, end)、指紋が steps(1, end), $ease-spring と、それぞれの対で指定する形です。02の三つのパネルも同じ配線で steps(1, end), $easing となり、目盛りバーは84%から86%へ三つ同時に戻します。通過した痕跡が一度に消える方が残像が少ないためです。01の戻る道は同じスプリングのイージングで押し戻し、瞬間移動に見えないようにして、三行目が再び広がった後の87%で背景だけを単独で消します。ログアウトを押したときの通知文も46%から48%の二ポイントの間でカットによって灯し、84%まで保ちます。半透明の中間フレームを作らないためです。02の六桁の欄には inputmode="numeric" と maxlength="6" を併せて置き、モバイルのキーボードが数字パッドで先に開くようにしました。フォーカスを移すときは focus({ preventScroll: true }) で親ページが引きずられないように防いでいます。デモはホームの一覧にiframeで埋め込まれているので、中でフォーカスが動くたびに上のページが勝手にスクロールされては困るのです。09のタイピング表示は等幅の ch 単位で幅を切り、六文字が正確に一コマずつ現れます。キャレットの点滅は400ms周期で、2秒のループの中にちょうど五周収まるので継ぎ目で点滅が途切れません。覆いの下の値も同じ等幅で組んであるので、幅の揃った十九文字が一コマごとに顔を出します。9つ分の規則を一つのフォルダにまとめた圧縮ファイルのパスワードは eeu22zhr で、中にはいま見ている素の版とReact版が同じ値で入っています。
アクセシビリティ
prefers-reduced-motion: reduce をオンにした画面では、9つすべてのループが止まり、結果の状態だけが残ります。止めるだけで終わりにせず、到着点を固定します。05は目盛りの四つが全部埋まり保存ボタンが活性色の終わった状態で、08は四つの行がすべてくっきり残り危ない行にだけ赤い枠が残る状態で、09は確認ダイアログが開いた画面で止まります。reduced-motionのブロック内の宣言にはすべて !important を付けました。付けないと animation: none は (0,3,0)の詳細度に負けて、260番で実測されたようにアニメーションが回り続けます。
スイッチは飾った <div> ではなく、role="switch" を持つ本物の <button> なので、SpaceとEnterがそのままclickとして届きます。施錠は本物の disabled ではなく aria-disabled を使いました(05・07・09)。ボタンが消えると、キーボードの利用者は急にTabが飛ぶ理由を知れません。フォーカスは残して、押したときの理由の文は05では aria-describedby の指す行に、09ではボタンの隣に現れます。削除の確認ダイアログは role="alertdialog" で、名前も説明も画面にすでにある文字を aria-labelledby と aria-describedby で指します。開けばフォーカスはその中へ移り、Tabは二つのボタンの間だけを巡ります。
| 部品 | 目で見ると | 読み上げでは | 操作 |
|---|---|---|---|
| 01 接続端末の一覧 | 行が滑り出て畳まる | role="status" がログアウトを読む |
ボタンを押す |
| 02 認証の三手順 | 目盛りが一つずつ埋まる | aria-current="step" が手順ごとに移動 |
次へ · Enter |
| 03 バックアップコード | 使ったコードに線 | role="status" がコピーの瞬間を読む |
すべてコピー |
| 04 パスキー登録 | 指紋が膨らみ盾へカット | aria-busy が待ち時間を知らせる |
登録を押す |
| 05 パスワード変更 | 目盛り四つ · 赤い不一致行 | aria-describedby の行が理由を答える |
一文字ごと |
| 06 通知スイッチ | つまみが滑る | role="switch" + aria-checked |
押す · Space |
| 07 アプリパスワード | 覆いが16コマで剥がれる | role="status" が発行とコピーを読む |
発行 → コピー |
| 08 ログイン履歴 | 危ない行に枠と呼吸 | 行が aria-pressed で確認を示す |
行を押す |
| 09 危険な操作 | DELETEを打たないと開かない | alertdialog とフォーカスの閉じ込め |
入力 · Escape |
スイッチの役割の付け方は MDNのswitchロールに、ダイアログを警告として開く方法は alertdialogロールにまとまっています。ログイン画面のカードは ログイン画面デザイン9選に、本人確認のカードは 本人確認画面UI 9選に集まっています。
FAQ
二段階認証の手順はCSSだけでは進めませんか?
目盛りまでは進みます。scaleX で一つずつ埋めていくのは純粋なCSSです。ただし、手順を正しく通過したかの判定はCSSにはできません。六桁のコードが本物と合っているか比べ、その結果を次の手順の開放に反映する仕事はJavaScriptが担います。02が aria-current="step" をスクリプトで動かす理由です。目盛りは見た目で、手順は状態です。
バックアップコードのコピーが失敗するブラウザはありますか?
あります。navigator.clipboard はセキュアコンテキストでしか存在しません。なので03は呼ぶ前に navigator.clipboard && navigator.clipboard.writeText を確認し、なければクリップボードには触らず、ラベルと読み上げの文だけ切り替えます。利用者にエラーを投げる代わりに、画面自身が状態を知らせる道を選びました。実際のサービスなら、ここに手動で選んでコピーする案内を一行足す方が安全です。
端末の行を消すとき、なぜ高さまで畳むのですか?
行だけを横に滑らせて空間を残すと、一覧に穴が開いたままになります。行の height は内容に合わせて自動的に決まる値なので、目標値なしには動かせません。そこで max-height を行の高さから0まで縮めて、下の行が滑らかに持ち上がるようにしています。01は滑り出す区間と畳まる区間を一つのキーフレームの中に並べて置き、戻る道も同じイージングで押し戻して瞬間移動に見えないようにしました。一覧全体の高さは固定してあるので、行が畳まれてもカードの他の部分は動きません。