適応型でアクセシビリティの高いトースト コンポーネントを構築する方法の基本的な概要。
この記事では、トースト コンポーネントの構築方法について説明します。デモをお試しください。
動画でご覧になりたい場合は、こちらの YouTube 版をご覧ください。
概要
トーストは、ユーザー向けの非インタラクティブで受動的な非同期の短いメッセージです。一般に、アクションの結果をユーザーに知らせるためのインターフェース フィードバック パターンとして使用されます。
インタラクション数
トーストは、通知、アラート、プロンプトとは異なり、インタラクティブではなく、閉じたり永続化したりすることを想定していません。通知は、重要な情報、操作が必要な同期メッセージ、システムレベルのメッセージ(ページレベルのメッセージとは異なる)を対象としています。トーストは、他の通知戦略よりも受動的です。
マークアップ
<output>
要素は、スクリーン リーダーに読み上げられるため、トーストに適しています。正しい HTML は、JavaScript と CSS で強化するための安全な基盤となります。JavaScript はたくさんあります。
トースト
<output class="gui-toast">Item added to cart</output>
role="status"
を追加すると、より包括的になります。これにより、ブラウザが仕様に沿って <output>
要素に暗黙的なロールを付与しない場合のフォールバックが提供されます。
<output role="status" class="gui-toast">Item added to cart</output>
トースト コンテナ
複数のトーストを同時に表示できます。複数のトーストをオーケストレートするために、コンテナが使用されます。このコンテナは、画面上のトーストの位置も処理します。
<section class="gui-toast-group">
<output role="status">Wizard Rose added to cart</output>
<output role="status">Self Watering Pot added to cart</output>
</section>
レイアウト
トーストをビューポートの inset-block-end
に固定することにしました。トーストが追加されると、その画面の端から積み重ねられます。
GUI コンテナ
トースト コンテナは、トーストを表示するためのレイアウト作業をすべて行います。ビューポートに対して fixed
であり、論理プロパティ inset
を使用して、固定するエッジと、同じ block-end
エッジからの padding
を指定します。
.gui-toast-group {
position: fixed;
z-index: 1;
inset-block-end: 0;
inset-inline: 0;
padding-block-end: 5vh;
}
トースト コンテナは、ビューポート内に配置されるだけでなく、トーストを整列して配置できるグリッド コンテナでもあります。アイテムは、justify-content
でグループとして中央に配置され、justify-items
で個別に中央に配置されます。トーストが触れないように、gap
を少し入れます。
.gui-toast-group {
display: grid;
justify-items: center;
justify-content: center;
gap: 1vh;
}
GUI トースト
個々のトーストには、いくつかの padding
、border-radius
を使用した角の丸み、モバイルとデスクトップのサイズ調整を支援する min()
関数があります。次の CSS のレスポンシブ サイズにより、トーストがビューポートの 90% または 25ch
より大きくならないようにします。
.gui-toast {
max-inline-size: min(25ch, 90vw);
padding-block: .5ch;
padding-inline: 1ch;
border-radius: 3px;
font-size: 1rem;
}
スタイル
レイアウトと位置を設定したら、ユーザー設定や操作への適応に役立つ CSS を追加します。
トースト コンテナ
トーストはインタラクティブではなく、タップやスワイプをしても何も起こりませんが、現在はポインタ イベントを消費します。次の CSS を使用して、トーストがクリックを奪うのを防ぎます。
.gui-toast-group {
pointer-events: none;
}
GUI トースト
カスタム プロパティ、HSL、設定メディアクエリを使用して、トーストにライトまたはダークの適応型テーマを適用します。
.gui-toast {
--_bg-lightness: 90%;
color: black;
background: hsl(0 0% var(--_bg-lightness) / 90%);
}
@media (prefers-color-scheme: dark) {
.gui-toast {
color: white;
--_bg-lightness: 20%;
}
}
アニメーション
新しいトーストは、画面に表示されるときにアニメーションで表示される必要があります。モーションの低減に対応するには、デフォルトで translate
の値を 0
に設定しますが、モーション設定のメディアクエリでモーションの値を長さに更新します。すべてのアニメーションが表示されますが、トーストが移動するのは一部のユーザーのみです。
トースト アニメーションに使用されるキーフレームは次のとおりです。CSS は、トーストの表示、待機、非表示をすべて 1 つのアニメーションで制御します。
@keyframes fade-in {
from { opacity: 0 }
}
@keyframes fade-out {
to { opacity: 0 }
}
@keyframes slide-in {
from { transform: translateY(var(--_travel-distance, 10px)) }
}
トースト要素は、変数を設定してキーフレームを調整します。
.gui-toast {
--_duration: 3s;
--_travel-distance: 0;
will-change: transform;
animation:
fade-in .3s ease,
slide-in .3s ease,
fade-out .3s ease var(--_duration);
}
@media (prefers-reduced-motion: no-preference) {
.gui-toast {
--_travel-distance: 5vh;
}
}
JavaScript
スタイルとスクリーン リーダーでアクセス可能な HTML が準備できたら、ユーザー イベントに基づいてトーストの作成、追加、破棄を調整するために JavaScript が必要になります。トースト コンポーネントの開発者エクスペリエンスは、次のように最小限で、簡単に始められるようにする必要があります。
import Toast from './toast.js'
Toast('My first toast')
トースト グループとトーストの作成
トースト モジュールが JavaScript から読み込まれる場合、トースト コンテナを作成してページに追加する必要があります。body
の前に要素を追加することにしました。これにより、コンテナがすべての body 要素のコンテナの上にあるため、z-index
のスタッキングの問題が発生する可能性が低くなります。
const init = () => {
const node = document.createElement('section')
node.classList.add('gui-toast-group')
document.firstElementChild.insertBefore(node, document.body)
return node
}
init()
関数はモジュールの内部で呼び出され、要素を Toaster
として保存します。
const Toaster = init()
トースト HTML 要素の作成は createToast()
関数で行われます。この関数は、トーストのテキストを必要とし、<output>
要素を作成し、いくつかのクラスと属性で装飾し、テキストを設定して、ノードを返します。
const createToast = text => {
const node = document.createElement('output')
node.innerText = text
node.classList.add('gui-toast')
node.setAttribute('role', 'status')
return node
}
1 つまたは複数のトーストの管理
JavaScript で、トーストを格納するためのコンテナがドキュメントに追加され、作成されたトーストを追加できるようになりました。addToast()
関数は、1 つまたは複数のトーストの処理をオーケストレートします。まず、トーストの数とモーションが問題ないかどうかを確認し、この情報を使用してトーストを追加するか、他のトーストが新しいトーストのために「スペースを空ける」ように見えるような凝ったアニメーションを行います。
const addToast = toast => {
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Toaster.children.length && motionOK
? flipToast(toast)
: Toaster.appendChild(toast)
}
最初のトーストを追加するとき、Toaster.appendChild(toast)
はページにトーストを追加し、CSS アニメーション(インアニメーション、3s
待機、アウトアニメーション)をトリガーします。既存のトーストがある場合は flipToast()
が呼び出され、Paul Lewis 氏が FLIP と呼ぶ手法が採用されます。新しいトーストが追加される前後のコンテナの位置の差を計算します。トースターの現在地と移動先をマークし、現在地から移動先までをアニメーションで表示するようなものです。
const flipToast = toast => {
// FIRST
const first = Toaster.offsetHeight
// add new child to change container size
Toaster.appendChild(toast)
// LAST
const last = Toaster.offsetHeight
// INVERT
const invert = last - first
// PLAY
const animation = Toaster.animate([
{ transform: `translateY(${invert}px)` },
{ transform: 'translateY(0)' }
], {
duration: 150,
easing: 'ease-out',
})
}
CSS グリッドはレイアウトの処理を行います。新しいトーストが追加されると、グリッドはそれを先頭に配置し、他のトーストとの間にスペースを入れます。一方、ウェブ アニメーションを使用して、コンテナを古い位置からアニメーション化します。
すべての JavaScript をまとめる
Toast('my first toast')
が呼び出されると、トーストが作成されてページに追加され(新しいトーストに対応するためにコンテナがアニメーション化されることもあります)、Promise が返され、作成されたトーストが Promise の解決のために CSS アニメーションの完了(3 つのキーフレーム アニメーション)を監視されます。
const Toast = text => {
let toast = createToast(text)
addToast(toast)
return new Promise(async (resolve, reject) => {
await Promise.allSettled(
toast.getAnimations().map(animation =>
animation.finished
)
)
Toaster.removeChild(toast)
resolve()
})
}
このコードのわかりにくい部分は、Promise.allSettled()
関数と toast.getAnimations()
マッピングにあると感じました。トーストに複数のキーフレーム アニメーションを使用しているため、すべてが完了したことを確実に知るには、それぞれを JavaScript からリクエストし、それぞれの finished
Promise の完了を監視する必要があります。allSettled
は、すべての Promise が満たされると、完了として解決されます。await Promise.allSettled()
を使用すると、次のコード行で要素を安全に削除し、トーストのライフサイクルが完了したと想定できます。最後に、resolve()
を呼び出すと、上位レベルの Toast Promise が満たされるため、デベロッパーはトーストが表示されたらクリーンアップやその他の作業を行うことができます。
export default Toast
最後に、Toast
関数がモジュールからエクスポートされ、他のスクリプトがインポートして使用できるようになります。
Toast コンポーネントの使用
トーストまたはトーストのデベロッパー エクスペリエンスを使用するには、Toast
関数をインポートし、メッセージ文字列を指定して呼び出します。
import Toast from './toast.js'
Toast('Wizard Rose added to cart')
トーストが表示された後にクリーンアップなどの処理を行いたい場合は、非同期処理と await を使用できます。
import Toast from './toast.js'
async function example() {
await Toast('Wizard Rose added to cart')
console.log('toast finished')
}
まとめ
私がどのように作成したかをご理解いただけたかと思います。では、あなたならどのように作成しますか?🙂
アプローチを多様化し、ウェブで構築するさまざまな方法を学びましょう。デモを作成して、ツイートでリンクを送信してください。下のコミュニティ リミックス セクションに追加します。
コミュニティ リミックス
- @_developit(HTML/CSS/JS を使用): デモとコード
- HTML/CSS/JS を使用した Joost van der Schee: デモとコード