アラート・バナー
情報・成功・警告・エラーの4色のインラインバナー。アイコンと文言、×ボタンで構成し、×を押すとスライドしながら消える。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- 保存完了・容量警告など画面内に留める通知
- ユーザーが閉じるまで残すべき警告・エラー
- ページ上部の更新案内
- 避ける場面
- 数秒で消えてよい軽い完了通知(トースト)
- 毎回同じ内容を出し続ける(バナー疲れ)
- role=alert を同時に複数出す
- 構成要素
- バナー
- 種別アイコン
- メッセージ
- 閉じるボタン
- 再表示ボタン
- 持っている状態
- 通常
- ホバー
- 押下中
- エラー
- 成功
- キーボード
Tab で各×ボタン・「すべて再表示」に到達・Enter/Space で閉じる/再表示する。閉じたバナーの×には Tab が入らない
- 支援技術
info/success は role=status(polite)、warning/error は role=alert(assertive)。×は aria-label「閉じる」、アイコンは aria-hidden。閉じたバナーは inert。色に加えアイコンで種別を示す
- 動き
閉じる時 220ms フェード+左 16px スライド・280ms で高さと余白を畳む。隙間を残さず消えたことを示す。reduce で停止
デザインの要点
- 種別は色+左の縁線+アイコンの 3 つで示す。青と緑は色覚特性によっては同じに見える
- 役割を分ける。info/success は role=status で順番に、warning/error は role=alert で割り込んで読ませる
- 閉じるときは max-height と padding も 280ms で畳む。フェードだけだと下のバナーが跳ね上がって見える
- 閉じたバナーは inert にして DOM に残す。opacity:0 の中の×ボタンに Tab が入ると、見えない要素を押すことになる
使いどころ
システム側から発生する状態通知(バージョン更新のお知らせ、保存完了、容量警告、送信失敗)を画面内に留めて表示したい場面に向く。トーストと違って自動では消えず、ユーザーが×を押すまで画面に残り続けるため、見落としても後から気付いて対処できる。特にwarning/errorのように行動を促したい通知は、数秒で消えるトーストより本実装のような滞留型のバナーの方が適している。逆に、ページ読み込みのたびに同じ通知を出し続けると次第に無視される「バナー疲れ」を起こすため、一度閉じたら再訪時には出さない(localStorage等での既読管理)といった工夫が要る。また4色を同時に大量に並べると重要度の区別がつかなくなるので、同じ画面に出すのはerrorとwarningのように優先度の高いものを中心に絞るのが実用的である。
仕組み
4種のバナーはalert--info/success/warning/errorのモディファイアクラスで色(border-left・文字色・背景)だけを変えており、構造とアニメーションは共通。閉じるときはis-dismissedクラスを付けるだけで、opacity・transform: translateX(-16px)・max-height: 0・padding・margin-bottomの5つのプロパティを同時にtransitionさせることで、フェード+左スライド+高さの折りたたみが一体になった消滅アニメーションを実現している。max-heightをアニメーションさせる手法は、height: autoが直接transitionできないCSSの制約を回避する定番のテクニックで、実測の高さより十分大きい80pxを初期値に設定してある。JS側はalerts要素へのイベント委任(closest(".alert-close"))で1つのリスナーが全バナーの×ボタンを処理し、閉じた件数が0になったら「すべて再表示」ボタンをhidden属性の切り替えで出す。
実装の注意
max-height: 80pxで折りたたみアニメーションを実現している都合上、メッセージ文言が長くて実際の高さが80pxを超えると、閉じる際に本文が窮屈に見えたり、逆に開いている間も本文が切れる可能性がある。長文を扱う場合はmax-heightの値をメッセージの想定行数に応じて引き上げる必要がある。次に、is-dismissedはopacityとpointer-eventsを操作するだけでDOMからは要素を削除しないため、閉じたバナーの数が多い一覧では非表示要素がDOMに残り続ける。長時間開いたページで何十件も通知を出しては閉じるような使い方をすると、要素が積み上がっていく点は把握しておくべきである。また、reset-btnの表示条件はquerySelectorAllで毎回全件を数えており、バナーの数が多い画面で頻繁に開閉すると、その都度DOM走査が走る点もパフォーマンス上の考慮点になる。
アクセシビリティ
role属性が種類によって使い分けられているのがこの実装の要点で、info/successにはrole="status"、warning/errorにはrole="alert"が付与されている。role="status"はaria-live="polite"相当で、読み上げ中の内容を妨げずに後で伝えられる程度の重要度、role="alert"はaria-live="assertive"相当で、読み上げ中でも即座に割り込んで伝えるべき緊急度の通知に使う使い分けが実装に反映されている。閉じるボタンはaria-label="閉じる"で意図が伝わり、アイコンsvgはaria-hidden="true"で読み上げから除外されている。@media (prefers-reduced-motion: reduce)でtransition: noneに倒す分岐も用意済みで、消滅時のスライド・折りたたみを含め動きに敏感なユーザーへの配慮ができている。ただし4種のバナーが同時に4つ表示される初期状態だと、role="alert"が2つ同時に読み上げに割り込む形になるため、実運用では同時に出すalertの数を絞るべきである。
調整して使う
触りやすいのは3点。まず色と種類の追加で、既存の4色(info青・success緑・warning橙・error赤)の構成を保ったまま、border-left色とbackground色のペアを増やせば新しい種類(例: お知らせ用の紫)を同じ構造で足せる。次に自動消滅の有無で、現状は手動で×を押すまで残り続ける設計だが、successのような重要度の低い通知だけトースト同様にsetTimeoutで自動的にis-dismissedを付ける分岐を足すこともできる。最後に配置で、このサンプルはページ内にstackして並ぶインラインの配置だが、position: fixedにして画面上部へ固定すればグローバルな通知帯としても使える(その場合はz-indexの管理とsafe-area-inset-topへの配慮が必要になる)。role="alert"を使う種類は特に、同時に複数出さない運用ルールとセットで設計するのが実用的である。
コード(コピーして使えます)
<div class="stage" id="stage">
<div class="alerts" id="alerts">
<div class="alert alert--info" role="status">
<span class="alert-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="7.5" stroke="currentColor" stroke-width="1.6"/>
<line x1="9" y1="8" x2="9" y2="13" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="9" cy="5.4" r="1" fill="currentColor"/>
</svg>
</span>
<p class="alert-msg">新しいバージョンが利用できます。</p>
<button class="alert-close" aria-label="閉じる">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="3" y1="3" x2="11" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<line x1="11" y1="3" x2="3" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="alert alert--success" role="status">
<span class="alert-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="7.5" stroke="currentColor" stroke-width="1.6"/>
<polyline points="5.5,9.2 8,11.5 12.5,6.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<p class="alert-msg">保存が完了しました。</p>
<button class="alert-close" aria-label="閉じる">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="3" y1="3" x2="11" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<line x1="11" y1="3" x2="3" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="alert alert--warning" role="alert">
<span class="alert-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M9 2.5l6.5 12h-13z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<line x1="9" y1="7" x2="9" y2="10.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="9" cy="12.8" r="1" fill="currentColor"/>
</svg>
</span>
<p class="alert-msg">まもなく容量の上限に達します。</p>
<button class="alert-close" aria-label="閉じる">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="3" y1="3" x2="11" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<line x1="11" y1="3" x2="3" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="alert alert--error" role="alert">
<span class="alert-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="7.5" stroke="currentColor" stroke-width="1.6"/>
<line x1="6" y1="6" x2="12" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<line x1="12" y1="6" x2="6" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</span>
<p class="alert-msg">送信に失敗しました。もう一度お試しください。</p>
<button class="alert-close" aria-label="閉じる">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="3" y1="3" x2="11" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<line x1="11" y1="3" x2="3" y2="11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<button class="reset-btn" id="resetBtn" hidden>すべて再表示</button>
</div>