Popover
クリックで開閉するポップオーバーを作成できるコンポーネントです。
Overview
ポップオーバーの中身です。外側のクリックやEscキーで閉じられます。
<Popover.Root popoverId="pop-01"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">Open Popover</Popover.Trigger> <Popover.Popup max-w="20rem"> <p>ポップオーバーの中身です。外側のクリックやEscキーで閉じられます。</p> </Popover.Popup></Popover.Root><div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-01">Open Popover</button> <div class="b--popover_popup -max-w" id="pop-01" popover="auto" data-side="bottom" data-align="center" style="--max-w:20rem"> <p>ポップオーバーの中身です。外側のクリックやEscキーで閉じられます。</p> </div></div>ブラウザ標準の Popover API(popover属性とpopovertarget属性)だけで動作するため、クライアント側のJavaScriptは一切必要ありません。
初期値のtype="auto"では、以下の動作はすべてブラウザが行います。
- トリガーのクリックによる開閉
- 外側のクリックやEscキーで閉じる(light dismiss)
- 閉じたあと、トリガーへフォーカスを戻す
- トリガーの
aria-expandedの切り替え
Tabキーでフォーカスがポップアップの外へ移動しても閉じません(Popover APIの仕様です)。フォーカスが外れたときに閉じたい場合は、focusoutイベントでhidePopover()を呼ぶなど、利用側で実装してください。
<Popover.Root>の中に<Popover.Trigger>と<Popover.Popup>を置くと、popovertargetとidが自動で紐付きます。<Popover.Close>も同様です。
- ポップアップは閉じている間、支援技術からも見えません。重要な操作や情報をポップアップの中だけに置かないでください。
<Popover.Trigger>と<Popover.Close>はbutton要素である必要があります。popovertarget属性はbutton(およびinput type="button"など)でしか機能しません。<Popover.Popup>にはroleを指定していません。フォームなど、まとまった操作を含む場合はrole="dialog"とaria-labelを指定してください。
Styles
Popover のベーススタイルは、以下のCSSで定義されています。
/*
* ネイティブ Popover API(popover 属性)で開閉し、CSS Anchor Positioning で配置する。
* anchor 配置は @supports で囲い、非対応ブラウザ(部分対応の Chrome 128〜130 を含む)には
* UA 既定の中央配置(inset: 0; margin: auto)に戻してカードとして整えるだけにする。
*/
@layer lism-block {
/* 調整用の変数は Root で受ける(Root の props がインラインに書く --popover-* をここで拾う) */
.b--popover {
/*
* 矢印の縁は popup の -bd(--bdc / --bdw)を拾う。外側から継承した値で縁取りしないよう Root で未定義に戻す
* (空値 `--bdc: ;` は未定義扱いにならず var() のフォールバックが効かないので initial を使う)。
*/
--bdc: initial;
--bdw: initial;
--duration: var(--popover-duration, 0.15s);
--offset: var(--popover-offset, var(--s5));
--arrow-sz: var(--popover-arrow, 6px); /* 矢印の高さ(突出量)。0 で矢印なし */
--popup-gap: calc(var(--offset) + var(--arrow-sz)); /* popup 本体とトリガーの間隔。offset は矢印の先とトリガーの間隔 */
display: inline-block;
anchor-scope: --popover, --popover-popup;
}
.b--popover_trigger {
anchor-name: --popover;
}
.b--popover_popup {
border: none;
font-size: var(--fz--s);
color: var(--text);
background-color: var(--base);
box-shadow: var(--bxsh--30);
overflow: auto;
padding: 0.75em 1em;
opacity: 0;
transition:
opacity var(--duration),
display var(--duration) allow-discrete,
overlay var(--duration) allow-discrete;
}
.b--popover_popup:popover-open {
opacity: 1;
}
@starting-style {
.b--popover_popup:popover-open {
opacity: 0;
}
}
/*
* anchor 配置。position-area は side と align の span を var() で合成し、
* start / end は span-* で片側に伸ばして place-self でアンカー端に揃える。
* span キーワードと揃える軸は side の系統(top / bottom と start / end)ごとに異なるので、
* 系統側で --_spanStart / --_spanEnd / --_placeStart / --_placeEnd の候補を定義し、align 側はそれを選ぶだけにする。
* top / bottom の align に span-x-* を使うのは、書字方向に従って RTL でも端に揃うため。
* 物理(top/bottom)と論理(span-inline-*)の混在は文法上無効で宣言ごと落ちるので使わない。
* side の start / end は inline 軸の論理方向(position-area の inline-start / inline-end)。block 軸は top / bottom の物理値のみ。
*/
@supports (anchor-name: --a) and (anchor-scope: all) and (position-area: inline-start span-block-end) and (position-try-fallbacks: flip-block) {
.b--popover_popup {
--_span: span-all;
anchor-name: --popover-popup; /* 矢印(::after)が popup の矩形を参照するため */
position-anchor: --popover;
position-area: var(--_side) var(--_span);
inset: auto;
}
/*
* ふきだしの矢印。ひし形の中心を「トリガー中心を popup の矩形にクランプした点」に置く。
* flip 後の位置は CSS から知れない(@position-try でカスタムプロパティは設定できない)ので、
* side / align / flip を見ずにトリガー側の辺へ寄るこの形にしている。
* absolute だと popup が包含ブロックになりトリガーを anchor に取れないため fixed(overflow: auto にもクリップされない)。
* スクロール追従は既定アンカー(position-anchor)にしか効かないので、トリガーを既定アンカーにして省略形の anchor() で参照する。
*
* 縁取りは 2 枚重ね。負の z-index は popup の背景・border より上に描かれるので、1 枚では内側半分が消せない。
* ::before(縁色)を border box に、::after(背景色)を border 幅ぶん内側の padding box にクランプすると、
* 外側の 2 辺だけ ::before がはみ出して縁になり、popup の border 線は ::after が上から消して口を開ける。
* 影は付けない(同じ理由で内側半分の影が popup の上に見える)。
*/
.b--popover_popup::before,
.b--popover_popup::after {
content: '';
position: fixed;
position-anchor: --popover;
z-index: -1; /* 中身の下、popup の背景・影の上 */
inline-size: calc(var(--arrow-sz) * 2);
block-size: calc(var(--arrow-sz) * 2);
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
translate: -50% -50%;
}
.b--popover_popup::before {
background-color: var(--bdc, inherit);
top: clamp(anchor(--popover-popup top), anchor(center), anchor(--popover-popup bottom));
left: clamp(anchor(--popover-popup left), anchor(center), anchor(--popover-popup right));
}
.b--popover_popup::after {
--_bdw: calc(var(--bdw, 0px) * 1.4142); /* 斜辺でも縁の太さが --bdw になるよう √2 倍ずらす */
background-color: inherit;
top: clamp(anchor(--popover-popup top) + var(--_bdw), anchor(center), anchor(--popover-popup bottom) - var(--_bdw));
left: clamp(anchor(--popover-popup left) + var(--_bdw), anchor(center), anchor(--popover-popup right) - var(--_bdw));
}
/* --_place*: place-self の値(align-self justify-self)。揃える軸だけ start / end にする */
.b--popover_popup:where([data-side='top'], [data-side='bottom']) {
--_spanStart: span-x-end;
--_spanEnd: span-x-start;
--_placeStart: auto start;
--_placeEnd: auto end;
margin-block: var(--popup-gap);
position-try-fallbacks:
flip-block,
flip-inline,
flip-block flip-inline;
}
.b--popover_popup:where([data-side='start'], [data-side='end']) {
--_spanStart: span-block-end;
--_spanEnd: span-block-start;
--_placeStart: start auto;
--_placeEnd: end auto;
margin-inline: var(--popup-gap);
position-try-fallbacks:
flip-inline,
flip-block,
flip-inline flip-block;
}
.b--popover_popup[data-side='top'] {
--_side: top;
}
.b--popover_popup[data-side='bottom'] {
--_side: bottom;
}
.b--popover_popup[data-side='start'] {
--_side: inline-start;
}
.b--popover_popup[data-side='end'] {
--_side: inline-end;
}
/* align: side 系統が用意した候補から選ぶ */
.b--popover_popup:where([data-align='start']) {
--_span: var(--_spanStart);
place-self: var(--_placeStart);
}
.b--popover_popup:where([data-align='end']) {
--_span: var(--_spanEnd);
place-self: var(--_placeEnd);
}
}
/* anchor 非対応: UA 既定の中央配置をカードとして整える */
@supports not (
(anchor-name: --a) and (anchor-scope: all) and (position-area: inline-start span-block-end) and (position-try-fallbacks: flip-block)
) {
.b--popover_popup {
margin: auto; /* lism-css の reset(*:not(dialog) { margin: 0 })で消える UA 既定値を戻して中央配置にする */
max-width: calc(100vw - var(--s20) * 2);
max-height: calc(100vh - var(--s20) * 2);
}
.b--popover_popup::backdrop {
background-color: rgb(0 0 0 / 0.2);
}
}
/* 減速設定時はフェードを無効化。インラインの --duration 指定よりも優先させるため !important */
@media (prefers-reduced-motion: reduce) {
.b--popover {
--duration: 0s !important;
}
}
} ソースコード全体は GitHub で公開しています。
How to use
@lism-css/ui パッケージでPopoverとして提供しています。
Import
import { Popover } from '@lism-css/ui/react'; import { Popover } from '@lism-css/ui/astro'; <link href="https://cdn.jsdelivr.net/npm/@lism-css/ui@0.29.0/dist/style.css" rel="stylesheet" /> <Popover.Root>, <Popover.Trigger>, <Popover.Popup>, <Popover.Close>が利用できます。
Props
| プロパティ | 説明 |
|---|---|
<Popover.Root> popoverId |
<Popover.Trigger> / <Popover.Close>のpopovertargetと、<Popover.Popup>のidに使われるIDを指定します。未指定の場合は自動生成されたIDが使われます。 |
<Popover.Root> offset |
トリガーとポップアップの間隔を指定します。インラインスタイルの--popover-offsetとして出力されます。 |
<Popover.Trigger> popoverId |
popovertargetの値として出力されます。<Popover.Root>の外で単体利用するときにだけ指定します。 |
<Popover.Popup> id |
id属性として出力されます。<Popover.Root>の外で単体利用するときにだけ指定します。 |
<Popover.Popup> side |
ポップアップを表示する向きを指定します。data-side属性として出力されます。指定できる値はtop / bottom / start / endで、初期値はbottomです。start / endは横方向の向きで、書字方向に追従します(dir="ltr"ではstartが左、endが右です)。 |
<Popover.Popup> align |
ポップアップの揃え位置を指定します。sideがtop / bottomのときは横方向、それ以外のときは縦方向の揃えです。data-align属性として出力されます。指定できる値はstart / center / endで、初期値はcenterです。 |
<Popover.Popup> type |
popover属性の値として出力されます。指定できる値はauto / manualで、初期値はautoです。manualにすると、外側のクリックやEscキーで閉じる動作(light dismiss)が無効になり、<Popover.Close>などのpopovertargetを持つボタンでしか閉じられなくなります。 |
<Popover.Close> popoverId |
popovertargetの値として出力されます。<Popover.Root>の外で単体利用するときにだけ指定します。 |
<Popover.Close> icon |
子要素を渡さない場合に表示されるアイコンを指定します。(初期値: x) |
<Popover.Close> srText |
アイコン表示時にスクリーンリーダー向けに出力されるテキストを指定します。初期値はCloseのため、日本語サイトではsrText="閉じる"の指定を推奨します。 |
<Popover.Root>の配下では、<Popover.Trigger> / <Popover.Popup> / <Popover.Close>に個別のIDを指定しないでください。IDを明示したい場合は<Popover.Root>のpopoverIdだけを使います。
子要素側のpopoverId / idは、<Popover.Root>を使わずにパーツを離れた場所へ配置する(各パーツに同じIDを手動で渡す)ための手段です。<Popover.Root>の配下で一部の子にだけ指定すると、紐付けが壊れてしまいます。
CSS変数
見た目の調整には、以下のCSS変数が使えます。いずれも<Popover.Root>(.b--popover)で受け取るため、<Popover.Root>またはその祖先要素に指定してください。<Popover.Popup>に指定しても効きません。
| 変数 | 初期値 | 説明 |
|---|---|---|
--popover-offset |
var(--s5) |
トリガーとポップアップの間隔。 |
--popover-duration |
0.15s |
開閉時のフェードにかかる時間。prefers-reduced-motion: reduceの環境では0sになります。 |
ポップアップの初期スタイルは、背景色var(--base)・余白1emに影を付けたカードです。角丸は初期状態では付いていません。配色や余白、角丸は、Lismのプロパティ(bgc, c, p, bdrs, bxshなど)で上書きしてください。
Examples
表示位置を変える
<Popover.Popup>のsideで表示する向きを、alignで揃え位置を指定できます。sideがtop / bottomのときは横方向、それ以外のときは縦方向の揃えになります。
sideとalignの指定例side=“bottom” align=“start”
side=“bottom” align=“end”
side=“top” align=“center”
side=“end” align=“start”
<Cluster g="15"> <Popover.Root popoverId="pop-02"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">bottom / start</Popover.Trigger> <Popover.Popup side="bottom" align="start" max-w="16rem"> <p>side="bottom" align="start"</p> </Popover.Popup> </Popover.Root> <Popover.Root popoverId="pop-03"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">bottom / end</Popover.Trigger> <Popover.Popup side="bottom" align="end" max-w="16rem"> <p>side="bottom" align="end"</p> </Popover.Popup> </Popover.Root> <Popover.Root popoverId="pop-04"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">top / center</Popover.Trigger> <Popover.Popup side="top" max-w="16rem"> <p>side="top" align="center"</p> </Popover.Popup> </Popover.Root> <Popover.Root popoverId="pop-05"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">end / start</Popover.Trigger> <Popover.Popup side="end" align="start" max-w="16rem"> <p>side="end" align="start"</p> </Popover.Popup> </Popover.Root></Cluster><div class="l--cluster -g:15"> <div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-02">bottom / start</button> <div class="b--popover_popup -max-w" id="pop-02" popover="auto" data-side="bottom" data-align="start" style="--max-w:16rem"> <p>side="bottom" align="start"</p> </div> </div> <div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-03">bottom / end</button> <div class="b--popover_popup -max-w" id="pop-03" popover="auto" data-side="bottom" data-align="end" style="--max-w:16rem"> <p>side="bottom" align="end"</p> </div> </div> <div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-04">top / center</button> <div class="b--popover_popup -max-w" id="pop-04" popover="auto" data-side="top" data-align="center" style="--max-w:16rem"> <p>side="top" align="center"</p> </div> </div> <div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-05">end / start</button> <div class="b--popover_popup -max-w" id="pop-05" popover="auto" data-side="end" data-align="start" style="--max-w:16rem"> <p>side="end" align="start"</p> </div> </div></div>sideのstart / endは書字方向に追従する論理的な向きです。dir="ltr"ではstartが左、endが右になり、dir="rtl"の環境では左右が入れ替わります。alignのstart / endも、sideがtop / bottomのときは同じように書字方向へ追従します。
また、画面の端に収まらないときは自動的に反対側へ反転します。
スタイルを変える
ポップアップの配色やborderは、Lismのプロパティで上書きできます。ふきだしの矢印は背景色に追従し、bdでborderを付けると縁の色と太さ(--bdc / --bdw)も矢印に反映されます。
bgc=“text” c=“base”
bd bdw=“2px” bdc=“current”
<Cluster g="15"> <Popover.Root popoverId="pop-07"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">配色を変える</Popover.Trigger> <Popover.Popup bgc="text" c="base" max-w="16rem"> <p>bgc="text" c="base"</p> </Popover.Popup> </Popover.Root> <Popover.Root popoverId="pop-08"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">borderを付ける</Popover.Trigger> <Popover.Popup bd bdw="2px" bdc="current" max-w="16rem"> <p>bd bdw="2px" bdc="current"</p> </Popover.Popup> </Popover.Root></Cluster><div class="l--cluster -g:15"> <div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-07">配色を変える</button> <div class="b--popover_popup -bgc:text -c:base -max-w" id="pop-07" popover="auto" data-side="bottom" data-align="center" style="--max-w:16rem"> <p>bgc="text" c="base"</p> </div> </div> <div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-08">borderを付ける</button> <div class="b--popover_popup -bd -bdc:current -max-w" id="pop-08" popover="auto" data-side="bottom" data-align="center" style="--bdw:2px;--max-w:16rem"> <p>bd bdw="2px" bdc="current"</p> </div> </div></div>閉じるボタンを置く(type="manual")
type="manual"を指定すると light dismiss が無効になり、外側のクリックやEscキーでは閉じられなくなります。必ず<Popover.Close>を配置してください。
type='manual'閉じるボタンを押すまで開いたままになります。
<Popover.Root popoverId="pop-06"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">お知らせを見る</Popover.Trigger> <Popover.Popup type="manual" max-w="20rem" bd> <Stack g="5"> <Flex ai="center" jc="between" g="15"> <span className="-fw:bold">お知らせ</span> <Popover.Close srText="閉じる" fz="l" hov="-c" /> </Flex> <p>閉じるボタンを押すまで開いたままになります。</p> </Stack> </Popover.Popup></Popover.Root><div class="b--popover"> <button class="b--popover_trigger set--plain -bd -px:15 -py:5 -bdrs:10 -hov:-bgc" type="button" popovertarget="pop-06">お知らせを見る</button> <div class="b--popover_popup -max-w -bd" id="pop-06" popover="manual" data-side="bottom" data-align="center" style="--max-w:20rem"> <div class="l--stack -g:5"> <div class="l--flex -ai:center -jc:between -g:15"> <span class="-fw:bold">お知らせ</span> <button class="b--popover_close set--plain -fz:l -hov:-c" type="button" popovertarget="pop-06" popovertargetaction="hide"> <svg class="a--icon" aria-hidden="true">...</svg> <span class="u--srOnly">閉じる</span> </button> </div> <p>閉じるボタンを押すまで開いたままになります。</p> </div> </div></div>ブラウザ対応
ポップアップの配置には CSS Anchor Positioning を使用しています。
未対応のブラウザ(Firefox ESR 140 など)では、ブラウザ既定の配置にフォールバックし、ポップアップは画面中央に表示されます。この場合、side / align / offset による位置調整は適用されません。
開閉・light dismiss・フォーカス制御は Popover API の機能なので、フォールバック時もそのまま動作します。