ポップアップ: 復活!

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

このデモが機能していることを確認するには、このデモの「全画面バージョン」を開く必要があります。

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

アンカー(開発中)

ポップオーバーに関しては、要素をトリガーに固定するという厄介なパターンがあります。たとえば、ツールチップがトリガーの上に表示される設定になっているにもかかわらず、ドキュメントがスクロールされた場合です。そのツールチップはビューポートで切り捨てられる可能性があります。現在、この問題に対処する JavaScript 機能(フローティング UI など)があります。その場合、ツールチップの位置が変わり、希望する掲載順位の順序に基づいてこれをやめることができます。

ただし、スタイルでこれを定義できるようにしたいと思っています。この問題に対処するために、Popover API とともにコンパニオン API が開発中です。「CSS Anchor Positioning」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 つを表示すると、他の 1 つは祖先ではないため、もう 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