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 トースト

個々のトーストには 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;
}

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()

Toast 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 LewisFLIP という手法が使用されます。新しいトーストの追加前後のコンテナの位置の差を計算します。トースターの現在地と移動先をマークし、移動先から現在地にアニメーション化することをイメージしてください。

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 が返され、作成されたトーストが CSS アニメーションの完了(3 つのキーフレーム アニメーション)を監視して Promise を解決します。

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 プロミスが完了したことを確認する必要があります。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')
}

まとめ

私の方法をご覧になったところで、あなたならどうしますか? 🙂?

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

コミュニティ リミックス