Popover
A component for creating popovers that open and close on click.
Overview
This is the popover content. Click outside or press Esc to close it.
<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>This is the popover content. Click outside or press Esc to close it.</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>This is the popover content. Click outside or press Esc to close it.</p> </div></div>It works using only the browser’s native Popover API (the popover and popovertarget attributes), so no client-side JavaScript is required.
With the default type="auto", the browser provides all of the following behavior:
- Opening and closing by clicking the trigger
- Closing on outside click or Esc (light dismiss)
- Returning focus to the trigger after closing
- Toggling
aria-expandedon the trigger
The popover does not close when focus moves outside it via the Tab key (this is how the Popover API behaves). If you want it to close on focus loss, implement it on your side, for example by calling hidePopover() on the focusout event.
Placing <Popover.Trigger> and <Popover.Popup> inside <Popover.Root> automatically links them via popovertarget and id. The same applies to <Popover.Close>.
- While closed, the popover is hidden from assistive technology too. Don’t put critical actions or information only inside a popover.
<Popover.Trigger>and<Popover.Close>must bebuttonelements. Thepopovertargetattribute only works onbutton(and elements likeinput type="button").<Popover.Popup>doesn’t set aroleby default. If it contains a self-contained set of controls, such as a form, addrole="dialog"andaria-label.
Styles
The Popover base styles are defined in the following 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;
}
}
} The full source code is available on GitHub.
Usage
Provided as Popover from the @lism-css/ui package.
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>, and <Popover.Close> are available.
Props
| Prop | Description |
|---|---|
<Popover.Root> popoverId |
Specifies the ID used for <Popover.Trigger> / <Popover.Close>’s popovertarget and <Popover.Popup>’s id. If omitted, an automatically generated ID is used. |
<Popover.Root> offset |
Specifies the gap between the trigger and the popup. Output as the --popover-offset variable. |
<Popover.Trigger> popoverId |
Output as the value of popovertarget. Only specify this when using the component standalone, outside <Popover.Root>. |
<Popover.Popup> id |
Output as the id attribute. Only specify this when using the component standalone, outside <Popover.Root>. |
<Popover.Popup> side |
Specifies the side the popup appears on. Output as the data-side attribute. Accepts top / bottom / start / end. start / end are the horizontal sides and follow the writing direction (start is left and end is right in dir="ltr"). Default: bottom. |
<Popover.Popup> align |
Specifies the alignment along the axis perpendicular to side. Output as the data-align attribute. Accepts start / center / end. Default: center. |
<Popover.Popup> type |
Output as the value of the popover attribute. Accepts auto / manual. Default: auto. Setting manual disables automatic closing (light dismiss) on outside click or Esc, so the popover can only be closed with a button that has a popovertarget, such as <Popover.Close>. |
<Popover.Close> popoverId |
Output as the value of popovertarget. Only specify this when using the component standalone, outside <Popover.Root>. |
<Popover.Close> icon |
Specifies the icon shown when no children are passed. (Default: x) |
<Popover.Close> srText |
Specifies the text output for screen readers when the icon is displayed. (Default: Close) |
Don’t set individual IDs on <Popover.Trigger> / <Popover.Popup> / <Popover.Close> when they’re inside <Popover.Root>. If you need to set an explicit ID, use only the popoverId prop on <Popover.Root>.
The child-level popoverId / id props exist for placing the parts apart from each other without <Popover.Root> (passing the same ID to each part yourself). Setting them on only some children inside <Popover.Root> breaks the linkage.
CSS variables
Use the following CSS variables to adjust the appearance. They’re all read on <Popover.Root> (.b--popover), so set them on <Popover.Root> or one of its ancestors. Setting them on <Popover.Popup> has no effect.
| Variable | Default | Description |
|---|---|---|
--popover-offset |
var(--s5) |
The gap between the trigger and the popup. |
--popover-duration |
0.15s |
The duration of the open/close fade transition. Set to 0s when prefers-reduced-motion: reduce is set. |
By default, the popup is styled as a card with a var(--base) background, 1em padding, and a shadow. It has no rounded corners by default. Override the color, spacing, and corner radius using Lism properties (bgc, c, p, bdrs, bxsh, etc.).
Examples
Changing the popup position
Use the side prop on <Popover.Popup> to set the direction, and align to set the alignment along the perpendicular axis.
side and alignside=“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>The start / end values of side are logical directions that follow the writing direction: start is left and end is right in dir="ltr", and they swap sides when dir="rtl" is set. The start / end values of align follow the writing direction in the same way when side is top / bottom.
The popup also automatically flips to the opposite side when it doesn’t fit within the viewport.
Changing the popup style
You can override the popup’s color and border using Lism properties. The speech-bubble arrow follows the popup’s background color, and adding a border with bd also applies the border color and width (--bdc / --bdw) to the arrow.
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">Change the color</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">Add a 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">Change the color</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">Add a 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>Adding a close button (type="manual")
Setting type="manual" disables light dismiss, so the popover can no longer be closed by clicking outside or pressing Esc. Make sure to include a <Popover.Close>.
type='manual'Stays open until you press the close button.
<Popover.Root popoverId="pop-06"> <Popover.Trigger bd px="15" py="5" bdrs="10" hov="-bgc">View announcement</Popover.Trigger> <Popover.Popup type="manual" max-w="20rem" bd> <Stack g="5"> <Flex ai="center" jc="between" g="15"> <span className="-fw:bold">Announcement</span> <Popover.Close fz="l" hov="-c" /> </Flex> <p>Stays open until you press the close button.</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">View announcement</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">Announcement</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">Close</span> </button> </div> <p>Stays open until you press the close button.</p> </div> </div></div>Browser support
Popover positioning uses CSS Anchor Positioning.
In browsers without support (e.g. Firefox ESR 140), it falls back to a card centered on the screen (the browser’s default placement). In this case, position adjustments via side / align / offset don’t apply.
Opening/closing, light dismiss, and focus management are all part of the Popover API, so they continue to work even in the fallback.