ポップアップ: 復活!

Open UI イニシアチブの目標は、デベロッパーが優れたユーザー エクスペリエンスを簡単に作成できるようにすることです。そのため、デベロッパーが直面する問題の多いパターンに対処しようとしています。これを行うには、プラットフォームの組み込み API とコンポーネントを改善します。

このような問題領域の 1 つがポップアップです。Open UI では「ポップオーバー」と説明されています。

ポップオーバーは、長い間、かなり両極端な評価を受けてきました。これは、構築とデプロイの両方の方法が原因の一部です。うまく構築するのは簡単ではありませんが、ユーザーを特定の場所に誘導したり、サイトのコンテンツをユーザーに認識させたりすることで、大きな価値を生み出すことができます。特に、上品な方法で使用すると効果的です。

ポップオーバーを構築する際には、次の 2 つの大きな懸念事項がよくあります。

  • コンテンツの残りの部分よりも上の適切な場所に配置されるようにする方法。
  • アクセス可能にする方法(キーボード操作に対応させる、フォーカス可能にするなど)。

組み込みの Popover API にはさまざまな目標がありますが、そのすべてに、デベロッパーがこのパターンを簡単に構築できるようにするという共通の目標があります。主な目標は次のとおりです。

  • 要素とその子孫をドキュメントの残りの部分の上に簡単に表示できるようにします。
  • アクセスしやすくする。
  • 最も一般的な動作(ライト ディスミス、シングルトン、スタッキングなど)に JavaScript を必要としない。

ポップアップの完全な仕様については、OpenUI サイトをご覧ください。

ブラウザの互換性

組み込みの Popover API はどこで使用できますか?執筆時点では、Chrome Canary の「試験運用版のウェブ プラットフォームの機能」フラグの背後でサポートされています。

このフラグを有効にするには、Chrome Canary を開いて chrome://flags にアクセスします。次に、「試験運用版のウェブ プラットフォームの機能」のフラグを有効にします。

本番環境でテストしたいデベロッパー向けに、オリジン トライアルも用意されています。

最後に、API 用のポリフィルが開発中です。github.com/oddbird/popup-polyfill でリポジトリをご確認ください。

ポップアップのサポート状況は、次のコマンドで確認できます。

const supported = HTMLElement.prototype.hasOwnProperty("popover");

現在のソリューション

コンテンツを最優先で宣伝するために、現在できることは何ですか?ブラウザでサポートされている場合は、HTML ダイアログ要素を使用できます。「モーダル」形式で使用する必要があります。これを使用するには JavaScript が必要です。

Dialog.showModal();

アクセシビリティに関する考慮事項がいくつかあります。たとえば、Safari バージョン 15.4 より前のユーザーに対応する場合は、a11y-dialog を使用することをおすすめします。

また、ポップオーバー、アラート、ツールチップ ベースのライブラリを使用することもできます。これらの多くは同様の動作をする傾向があります。

  • ポップオーバーを表示するために、本文にコンテナを追加します。
  • 他のすべての要素の上に表示されるようにスタイルを設定します。
  • 要素を作成してコンテナに追加し、ポップオーバーを表示します。
  • DOM からポップオーバー要素を削除して非表示にします。

これには、追加の依存関係とデベロッパーによるより多くの決定が必要です。また、必要なものをすべて提供するサービスを見つけるには、調査も必要です。Popover API は、ツールチップなど、さまざまなシナリオに対応することを目的としています。この目標は、一般的なシナリオをすべてカバーし、デベロッパーが別の決定を下す必要がないようにすることで、デベロッパーがエクスペリエンスの構築に集中できるようにすることです。

最初のポップアップ

必要なのはこれだけです。

<div id="my-first-popover" popover>Popover Content!</div>
<button popovertoggletarget="my-first-popover">Toggle Popover</button>

しかし、ここで何が起こっているのでしょうか?

  • ポップオーバー要素をコンテナなどに入れる必要はありません。デフォルトで非表示になっています。
  • 表示させるために JavaScript を記述する必要はありません。これは popovertoggletarget 属性で処理されます。
  • 表示されると、最上位レイヤに昇格します。つまり、ビューポート内の document より上に表示されます。z-index を管理したり、ポップオーバーが DOM のどこにあるかを気にしたりする必要はありません。クリッピングの祖先要素を持つ DOM の深いネスト構造に存在している可能性があります。DevTools を使用して、現在最上位レイヤにある要素を確認することもできます。最上位レイヤについて詳しくは、こちらの記事をご覧ください。

DevTools の最上位レイヤのサポートがデモされている GIF

  • 「ライト ディスミス」はすぐに利用できます。つまり、ポップオーバーの外側をクリックする、キーボードで別の要素に移動する、Esc キーを押すなどの終了シグナルでポップオーバーを閉じることができます。もう一度開いて、お試しください。

ポップオーバーには他にどのような機能がありますか?例をさらに詳しく見てみましょう。ページにコンテンツが含まれているこのデモを考えてみましょう。

このフローティング アクション ボタンは、高い z-index で固定配置されています。

.fab {
  position: fixed;
  z-index: 99999;
}

ポップオーバーのコンテンツは DOM にネストされますが、ポップオーバーを開くと、その固定位置要素の上に昇格します。スタイルを設定する必要はありません。

また、ポップオーバーに ::backdrop 疑似要素が追加されていることにも気づくでしょう。最上位レイヤにあるすべての要素には、スタイル設定可能な ::backdrop 疑似要素が適用されます。この例では、アルファ値を下げた背景色と背景フィルタを使用して ::backdrop をスタイル設定し、基盤となるコンテンツをぼかしています。

ポップオーバーのスタイル設定

では、ポップオーバーのスタイル設定に移りましょう。デフォルトでは、ポップオーバーは固定位置に配置され、パディングが適用されます。また、display: none もあります。これをオーバーライドしてポップオーバーを表示できます。ただし、最上位レイヤに昇格することはありません。

[popover] { display: block; }

ポップオーバーのプロモーション方法に関係なく、ポップオーバーを最上位レイヤにプロモートしたら、レイアウトまたは配置が必要になることがあります。最上位レイヤをターゲットにして、次のような処理を行うことはできません。

:open {
  display: grid;
  place-items: center;
}

デフォルトでは、ポップオーバーは margin: auto を使用してビューポートの中央にレイアウトされます。ただし、場合によっては、位置を明示的に指定する必要があります。次に例を示します。

[popover] {
  top: 50%;
  left: 50%;
  translate: -50%;
}

CSS グリッドまたは flexbox を使用してポップオーバー内のコンテンツをレイアウトする場合は、要素でラップすることをおすすめします。それ以外の場合は、ポップオーバーが最上位レイヤにあるときに display を変更する別のルールを宣言する必要があります。デフォルトで設定すると、display: none をオーバーライドしてデフォルトで表示されます。

[popover]:open {
 display: flex;
}

デモを試してみると、ポップオーバーがフェードイン / フェードアウトするようになっていることがわかります。:open 疑似セレクタを使用すると、ポップオーバーの表示と非表示を切り替えることができます。:open 疑似セレクタは、表示されている(つまり最上位レイヤにある)ポップオーバーに一致します。

この例では、カスタム プロパティを使用してトランジションを駆動しています。ポップオーバーの ::backdrop にも切り替え効果を適用できます。

[popover] {
  --hide: 1;
  transition: transform 0.2s;
  transform: translateY(calc(var(--hide) * -100vh))
            scale(calc(1 - var(--hide)));
}

[popover]::backdrop {
  transition: opacity 0.2s;
  opacity: calc(1 - var(--hide, 1));
}


[popover]:open::backdrop  {
  --hide: 0;
}

ここで、モーションのメディアクエリの下にトランジションとアニメーションをグループ化することをおすすめします。これにより、タイミングを維持することもできます。これは、カスタム プロパティを介して popover::backdrop の間で値を共有できないためです。

@media(prefers-reduced-motion: no-preference) {
  [popover] { transition: transform 0.2s; }
  [popover]::backdrop { transition: opacity 0.2s; }
}

これまでは、popovertoggletarget を使用してポップオーバーを表示する方法を見てきました。これを閉じるには、「ライト閉じる」を使用します。ただし、使用できる popovershowtarget 属性と popoverhidetarget 属性も取得できます。ポップオーバーにボタンを追加してポップオーバーを非表示にし、切り替えボタンを popovershowtarget を使用するように変更しましょう。

<div id="code-popover" popover>
  <button popoverhidetarget="code-popover">Hide Code</button>
</div>
<button popovershowtarget="code-popover">Reveal Code</button>

前述のように、Popover API は、従来のポップアップの概念だけでなく、より広範囲を対象としています。通知、メニュー、ツールチップなど、あらゆる種類のシナリオに対応したビルドが可能です。

このようなシナリオでは、異なるインタラクション パターンが必要になります。マウスオーバーなどの操作。popoverhovertarget 属性の使用がテストされましたが、現在は実装されていません。

<div popoverhovertarget="hover-popover">Hover for Code</div>

要素にカーソルを合わせるとターゲットが表示されるというアイデアです。この動作は CSS プロパティで構成できます。これらの CSS プロパティは、ポップオーバーが反応する要素のオンとオフを切り替える時間枠を定義します。テストされたデフォルトの動作では、:hover の明示的な 0.5s の後にポップオーバーが表示されました。その場合は、軽く閉じるか、別のポップオーバーを開いて閉じる必要があります(詳しくは後述)。これは、ポップオーバーの非表示期間が Infinity に設定されていたためです。

それまでの間、JavaScript を使用してその機能をポリフィルできます。

let hoverTimer;
const HOVER_TRIGGERS = document.querySelectorAll("[popoverhovertarget]");
const tearDown = () => {
  if (hoverTimer) clearTimeout(hoverTimer);
};
HOVER_TRIGGERS.forEach((trigger) => {
  const popover = document.querySelector(
    `#${trigger.getAttribute("popoverhovertarget")}`
  );
  trigger.addEventListener("pointerenter", () => {
    hoverTimer = setTimeout(() => {
      if (!popover.matches(":open")) popover.showPopover();
    }, 500);
    trigger.addEventListener("pointerleave", tearDown);
  });
});

明示的なホバー ウィンドウを設定するメリットは、ユーザーの操作が意図的なものであることを確認できることです(たとえば、ユーザーがポインタをターゲットの上に移動させるなど)。ユーザーが意図しない限り、ポップアップを表示したくありません。

ウィンドウが 0.5s に設定されている状態でターゲットにカーソルを合わせることができるデモをお試しください。


一般的なユースケースと例を説明する前に、いくつか確認しておきましょう。


ポップオーバーの種類

JavaScript 以外の操作の動作について説明しました。しかし、ポップオーバーの動作全体はどうでしょうか。「ライトを閉じる」を無効にするにはどうすればよいですか?または、ポップオーバーにシングルトン パターンを適用したい場合。

Popover API では、動作が異なる 3 種類のポップオーバーを指定できます。

[popover=auto]/[popover]:

  • ネストのサポート。これは、DOM にネストされていることを意味するだけではありません。祖先ポップオーバーの定義は次のとおりです。
    • DOM の位置(子)で関連付けられている。
    • popovertoggletargetpopovershowtarget などの子要素のトリガー属性によって関連付けられます。
    • anchor 属性で関連付けられている(開発中の CSS アンカー API)。
  • ライトを閉じる。
  • 開くと、祖先ポップオーバー以外の他のポップオーバーが閉じます。以下のデモで、祖先ポップオーバーを使用したネストの仕組みをご確認ください。popoverhidetarget/popovershowtarget インスタンスの一部を popovertoggletarget に変更すると、どのように変化するかを確認します。
  • ライトを 1 つ閉じるとすべて閉じますが、スタック内のライトを 1 つ閉じると、スタック内のそのライトより上のライトのみが閉じます。

[popover=manual]:

  • 他のポップオーバーを閉じません。
  • ライトをオフにできません。
  • トリガー要素または JavaScript を介して明示的に閉じる必要があります。

JavaScript API

ポップオーバーをより細かく制御する必要がある場合は、JavaScript を使用します。showPopover メソッドと hidePopover メソッドの両方が取得されます。また、リッスンする popovershow イベントと popoverhide イベントもあります。

ポップオーバーを表示する js popoverElement.showPopover() ポップオーバーを非表示にする:

popoverElement.hidePopover()

ポップオーバーが表示されるのをリッスンします。

popoverElement.addEventListener('popovershow', doSomethingWhenPopoverShows)

ポップオーバーが表示されるのをリッスンし、表示をキャンセルします。

popoverElement.addEventListener('popovershow',event => {
  event.preventDefault();
  console.warn(We blocked a popover from being shown);
})

ポップオーバーが非表示になるのをリッスンします。

popoverElement.addEventListener('popoverhide', doSomethingWhenPopoverHides)

非表示になっているポップオーバーをキャンセルすることはできません。

popoverElement.addEventListener('popoverhide',event => {
  event.preventDefault();
  console.warn("You aren't allowed to cancel the hiding of a popover");
})

ポップオーバーが最上位レイヤにあるかどうかを確認します。

popoverElement.matches(':open')

これにより、あまり一般的ではないシナリオで追加の電力を供給できます。たとえば、しばらく操作がないとポップオーバーを表示します。

このデモには、ポップ音が聞こえるポップオーバーがあるため、音声を再生するには JavaScript が必要になります。クリックすると、ポップオーバーが非表示になり、音声が再生され、再び表示されます。

ユーザー補助

Popover API では、アクセシビリティが最優先事項です。ユーザー補助マッピングは、必要に応じてポップオーバーをトリガー要素に関連付けます。つまり、popovertoggletarget などのトリガー属性のいずれかを使用している場合は、aria-haspopup などの aria-* 属性を宣言する必要はありません。

フォーカス管理では、オートフォーカス属性を使用して、ポップオーバー内の要素にフォーカスを移動できます。これはダイアログの場合と同じですが、フォーカスを戻すときに違いが生じます。これは、ライト ディスミスが原因です。ほとんどの場合、ポップオーバーを閉じると、フォーカスは以前にフォーカスされていた要素に戻ります。ただし、フォーカスを取得できる場合は、クリックされた要素にフォーカスが移動します。説明のフォーカス管理に関するセクションをご覧ください。

このデモの「全画面バージョン」を開いて、動作を確認する必要があります。

このデモでは、フォーカスされた要素に緑色の枠線が表示されます。キーボードでインターフェースをタブ移動してみてください。ポップオーバーが閉じられたときにフォーカスが戻る場所に注意してください。タブ移動すると、ポップオーバーが閉じることもあります。これは仕様です。ポップオーバーにはフォーカス管理がありますが、フォーカスをトラップしません。また、キーボード ナビゲーションでは、フォーカスがポップオーバーから移動したときに閉じるシグナルが識別されます。

アンカー(開発中)

ポップオーバーの場合、要素をトリガーに固定するという難しいパターンに対応する必要があります。たとえば、ツールチップがトリガーの上に表示されるように設定されているが、ドキュメントがスクロールされた場合などです。そのツールチップがビューポートで切り取られる可能性があります。この問題に対処するための JavaScript のソリューションとして、「Floating UI」などがあります。ツールチップの位置を変更して、この問題を回避し、目的の位置順序に依存します。

ただし、スタイルでこれを定義できるようにしたいと考えています。この問題に対処するため、Popover API と並行してコンパニオン API が開発されています。「CSS アンカー ポジショニング」API を使用すると、要素を他の要素に固定できます。この API は、要素がビューポートで切り取られないように要素の位置を変更する形で固定を行います。

このデモでは、現在の状態の Anchoring API を使用しています。ボートの位置は、ビューポート内のアンカーの位置に応じて変化します。

このデモを機能させる CSS のスニペットを次に示します。JavaScript は不要です。

.anchor {
  --anchor-name: --anchor;
}
.anchored {
  position: absolute;
  position-fallback: --compass;
}
@position-fallback --compass {
  @try {
    bottom: anchor(--anchor top);
    left: anchor(--anchor right);
  }
  @try {
    top: anchor(--anchor bottom);
    left: anchor(--anchor right);
  }
}

仕様はこちらで確認できます。この API のポリフィルも用意されます。

ポップオーバーの機能と仕組みを理解できたので、いくつかの例を見てみましょう。

通知

このデモでは、「クリップボードにコピーしました」という通知が表示されます。

  • [popover=manual] を使用する。
  • アクション時に showPopover のポップオーバーを表示します。
  • 2000ms タイムアウト後、hidePopover で非表示にします。

トースト

このデモでは、トップレイヤを使用してトースト スタイルの通知を表示します。

  • タイプ manual の 1 つのポップオーバーがコンテナとして機能します。
  • 新しい通知がポップオーバーに追加され、ポップオーバーが表示されます。
  • クリックすると、Web アニメーション API で削除され、DOM から削除されます。
  • 表示するトーストがない場合、ポップオーバーは非表示になります。

ネストされたメニュー

このデモでは、ネストされたナビゲーション メニューの動作を示します。

  • [popover=auto] を使用します。ネストされたポップオーバーが許可されるためです。
  • キーボードで移動できるように、各プルダウンの最初のリンクに autofocus を使用します。
  • これは CSS Anchoring API の対象として最適です。ただし、このデモでは、少量の JavaScript を使用して、カスタム プロパティを使用して位置を更新できます。
const ANCHOR = (anchor, anchored) => () => {
  const { top, bottom, left, right } = anchor.getBoundingClientRect();
  anchored.style.setProperty("--top", top);
  anchored.style.setProperty("--right", right);
  anchored.style.setProperty("--bottom", bottom);
  anchored.style.setProperty("--left", left);
};

PRODUCTS_MENU.addEventListener("popovershow", ANCHOR(PRODUCT_TARGET, PRODUCTS_MENU));

このデモでは autofocus を使用しているため、キーボード ナビゲーションを行うには「全画面表示」で開く必要があります。

メディア ポップオーバー

このデモでは、メディアをポップアップする方法を示します。

  • ライトの閉じるに [popover=auto] を使用します。
  • JavaScript は動画の play イベントをリッスンし、動画をポップアップします。
  • ポップオーバーの popoverhide イベントで動画が一時停止します。

Wiki スタイルのポップオーバー

このデモでは、メディアを含むインライン コンテンツ ツールチップを作成する方法を示します。

  • [popover=auto] を使用します。1 つを表示すると、他のものは祖先ではないため非表示になります。
  • JavaScript を使用して pointerenter に表示されます。
  • CSS アンカー API のもう 1 つの最適な候補です。

このデモでは、ポップオーバーを使用してナビゲーション ドロワーを作成します。

  • ライトの閉じるに [popover=auto] を使用します。
  • autofocus を使用して、最初のナビゲーション アイテムにフォーカスします。

背景の管理

このデモでは、複数のポップオーバーの背景を管理して、1 つの ::backdrop のみを表示する方法を示します。

  • JavaScript を使用して、表示されているポップオーバーのリストを維持します。
  • 最上位レイヤの最下位のポップオーバーにクラス名を適用します。

カスタム カーソルのポップオーバー

このデモでは、popover を使用して canvas を最上位レイヤに昇格させ、カスタム カーソルを表示する方法を示します。

  • showPopover[popover=manual] を使用して canvas を最上位レイヤに昇格させます。
  • 他のポップオーバーが開いている場合は、canvas ポップオーバーを非表示にしてから表示し、最前面に表示されるようにします。

アクションシートのポップオーバー

このデモでは、ポップオーバーをアクションシートとして使用する方法を示します。

  • display をオーバーライドして、デフォルトでポップオーバーを表示します。
  • アクションシートがポップオーバー トリガーで開きます。
  • ポップオーバーが表示されると、最上位レイヤに昇格し、ビューに変換されます。
  • ライトの閉じるを使用して戻すことができます。

キーボードが有効になったポップオーバー

このデモでは、コマンド パレット スタイルの UI にポップオーバーを使用する方法を示します。

  • cmd + j を使用してポップオーバーを表示します。
  • inputautofocus でフォーカスされます。
  • コンボボックスは、メイン入力の下に配置された 2 つ目の popover です。
  • ライトを閉じることで、ドロップダウンがない場合はパレットが閉じます。
  • Anchoring API の別の候補

タイマー付きポップオーバー

このデモでは、4 秒後に非アクティブ状態のポップオーバーが表示されます。ユーザーに関する安全な情報を保持するアプリで、ログアウト モーダルを表示するために頻繁に使用される UI パターン。

  • JavaScript を使用して、一定時間操作がない場合にポップオーバーを表示します。
  • ポップオーバーの表示時にタイマーをリセットします。

スクリーンセーバー

前のデモと同様に、サイトに遊び心を加え、スクリーン セーバーを追加できます。

  • JavaScript を使用して、一定時間操作がない場合にポップオーバーを表示します。
  • ライトを解除してタイマーを非表示にし、リセットします。

カーソルの追跡

このデモでは、ポップオーバーを入力キャレットに追従させる方法を示します。

  • 選択、キーイベント、特殊文字の入力に基づいてポップオーバーを表示します。
  • JavaScript を使用して、スコープ付きカスタム プロパティでポップオーバーの位置を更新します。
  • このパターンでは、表示されるコンテンツとユーザー補助について慎重に検討する必要があります。
  • テキスト編集 UI やタグ付け可能なアプリでよく見られます。

フローティング アクション ボタンのメニュー

このデモでは、JavaScript を使用せずにポップオーバーを使用してフローティング アクション ボタン メニューを実装する方法を示します。

  • showPopover メソッドを使用して、manual タイプのポップオーバーを昇格させます。これがメインボタンです。
  • メニューは、メインボタンのターゲットとなる別のポップオーバーです。
  • popovertoggletarget でメニューを開いた。
  • autofocus を使用して、表示された最初のメニュー項目にフォーカスします。
  • 軽くタップするとメニューが閉じます。
  • アイコンの回転には :has() を使用します。:has() について詳しくは、こちらの記事をご覧ください。

これを探してた!

以上が、Open UI イニシアチブの一環として今後導入される予定のポップオーバーの概要です。適切に使用すれば、ウェブ プラットフォームに素晴らしい機能が追加されることになります。

Open UI もぜひご覧ください。ポップオーバーの説明は、API の進化に合わせて最新の状態に保たれます。すべてのデモのコレクションはこちらです。

お立ち寄りいただきありがとうございます。