ポップアップ: 復活!

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

そのような問題領域の一つがポップアップです。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();

ユーザー補助に関する考慮事項がいくつかあります。たとえば、バージョン 15.4 より前の Safari のユーザーに対応する場合は、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 Anchoring 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 が必要です。クリックするとポップオーバーが非表示になり、音声が再生されて、再度表示されます。

ユーザー補助

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

フォーカス管理では、autofocus 属性を使用してポップオーバー内の要素にフォーカスを移動できます。これはダイアログの場合と同じですが、フォーカスを戻す際の違いがあります。これは、軽い閉じ方によるものです。ほとんどの場合、ポップオーバーを閉じると、フォーカスは以前にフォーカスされていた要素に戻ります。ただし、軽い閉じる操作でクリックされた要素にフォーカスが移動します(フォーカスを取得できる場合)。説明のフォーカス管理に関するセクションをご覧ください。

このデモを動作させるには、このデモの全画面バージョンを開く必要があります。

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

アンカー(開発中)

ポップオーバーの場合、要素をトリガーに固定するパターンに対応するのは難しい場合があります。たとえば、ツールチップがトリガーの上に表示される設定になっているにもかかわらず、ドキュメントがスクロールされた場合です。そのツールチップはビューポートで切り捨てられる可能性があります。現在、この問題に対処する JavaScript 機能(フローティング 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 Animations 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 Anchoring API に最適な候補です。

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

  • ライトの閉鎖に [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 タイプのポップオーバーを昇格させます。これがメインボタンです。
  • メニューは、メインボタンのターゲットとなるもう 1 つのポップオーバーです。
  • popovertoggletarget でメニューが開きます。
  • autofocus を使用して、番組の最初のメニュー項目にフォーカスします。
  • 軽く閉じるとメニューが閉じます。
  • アイコンのツイストには :has() を使用します。:has() について詳しくは、こちらの記事をご覧ください。

これで作業は完了です。

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

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

お問い合わせいただきありがとうございます。


写真提供: Madison OrenUnsplash