toast コンポーネントの作成

適応型でアクセシビリティの高いトースト コンポーネントを構築する方法の基本的な概要。

この記事では、トースト コンポーネントの構築方法について説明します。デモをお試しください。

デモ

動画でご覧になりたい場合は、こちらの 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;
}

.gui-toast-container 要素にオーバーレイされた DevTools のボックスサイズとパディングのスクリーンショット。

トースト コンテナは、ビューポート内に配置されるだけでなく、トーストを整列して配置できるグリッド コンテナでもあります。アイテムは、justify-content でグループとして中央に配置され、justify-items で個別に中央に配置されます。トーストが触れないように、gap を少し入れます。

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

トースト グループに CSS グリッドがオーバーレイされたスクリーンショット。今回は、トーストの子要素間のスペースとギャップがハイライト表示されています。

GUI トースト

個々のトーストには、いくつかの paddingborder-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;
}

1 つの .gui-toast 要素のスクリーンショット。パディングとボーダー半径が表示されています。

スタイル

レイアウトと位置を設定したら、ユーザー設定や操作への適応に役立つ 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
}

head タグと body タグの間のトースト グループのスクリーンショット。

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')
}

まとめ

私がどのように作成したかをご理解いただけたかと思います。では、あなたならどのように作成しますか?🙂

アプローチを多様化し、ウェブで構築するさまざまな方法を学びましょう。デモを作成して、ツイートでリンクを送信してください。下のコミュニティ リミックス セクションに追加します。

コミュニティ リミックス