A floating sheet anchored to the button that opens it.
On this page
A popover is a floating sheet that belongs to the button that opened it. Add the popover attribute and .popover to any element, and point a button at it with popovertarget. The browser moves it to the top layer, closes it on an outside click or Esc, and keeps it next in the tab order after its button. CSS anchor positioning places it, so there is no positioning script.
Popover
<buttontype="button"class="btn"popovertarget="share">Share</button><divid="share"popoverclass="popover"><divclass="flexflex-colgap-smw(260)"><pclass="m(0)fg-text-strong">Share this project</p><divclass="input-group input-group-sm"><inputclass="input-group-field"value="https://atlas.dev/p/9f2c"readonlyaria-label="Link"><buttontype="button"class="btn">Copy</button></div></div></div>
A popover opens 6px below its button and lines up with the button's start edge. The button is its anchor without any name: the popover API connects the two. When the popover would leave the viewport it flips to the other side.
Class
Placement
popover
Below, lined up with the start edge
popover-end
Lined up with the end edge, for a button at the end of a row
popover-center
Centered on the button
popover-top
Above the button; combine it with popover-end or popover-center
Placement
<buttontype="button"class="btn"popovertarget="place-start">Start</button><buttontype="button"class="btn"popovertarget="place-center">Center</button><buttontype="button"class="btn"popovertarget="place-end">End</button><buttontype="button"class="btn"popovertarget="place-top">Top</button><divid="place-start"popoverclass="popover">Lined up with the start edge.</div><divid="place-center"popoverclass="popover popover-center">Centered on the button.</div><divid="place-end"popoverclass="popover popover-end">Lined up with the end edge.</div><divid="place-top"popoverclass="popover popover-top popover-center">Above the button.</div>
In a browser without anchor positioning the popover still opens and closes; it appears centered in the viewport, where the browser places any popover.
A popover can also open when a link or a button is hovered or focused, as a preview of where it leads. Give it popover="hint" and point the trigger at it with interestfor. A hint does not close a menu or another popover that is already open. Keep the content a preview: a reader on a touch screen reaches it only by a long press, so nothing should be available here alone.
The popover component opens and closes the popover from the application's state and says when the browser did. The trigger and the content stay the page's own markup. Copy it into the project with npx master-ui add popover; React, Svelte and Vue says how the components are built and used.
ui/popover.tsx
'use client'import{ useEffect, useId, useReducer, useRef,type ComponentProps,type ReactNode }from'react'exportinterfacePopoverPropsextendsOmit<ComponentProps<'div'>,'popover'>{/** Whether the popover is open, when this component is told. Leave it out and the trigger opens it. */open?:booleanonOpenChange?:(open:boolean)=>void/** The button that opens the popover: spread what it is given on a button of your own. */trigger?:(attributes:{popoverTarget:string})=>ReactNode/** `auto` closes on a press outside and on Esc; `manual` stays until it is closed. */mode?:'auto'|'manual'}/** * A popover. The browser opens it from the trigger, places it against the trigger and dismisses it; this * component only opens and closes it from `open`, and says when the browser did. */exportfunctionPopover({ open, onOpenChange, trigger, mode ='auto', className, id, children,...root }:PopoverProps){const generated =useId()const name = id ?? generatedconst element =useRef<HTMLDivElement>(null)const heard =useRef(onOpenChange)/** What the owner knows: the last state it set or was told. */const known =useRef(false)const[tick, again]=useReducer((count:number)=> count +1,0)useEffect(()=>{ heard.current= onOpenChange })useEffect(()=>{const popover = element.currentif(!popover || open ===undefined)returnconst shown = popover.matches(':popover-open') known.current= openif(open &&!shown) popover.showPopover()elseif(!open && shown) popover.hidePopover()},[open, tick])useEffect(()=>{const popover = element.currentif(!popover)returnconstreport=()=>{const shown = popover.matches(':popover-open')if(shown !== known.current) heard.current?.(known.current= shown)// An `open` this component is told wins: if its owner did not take the change, the popover goes back.again()} popover.addEventListener('toggle', report)return()=> popover.removeEventListener('toggle', report)},[])return(<>{trigger?.({popoverTarget: name })}<divid={name}popover={mode}className={className ?`popover ${className}`:'popover'}ref={element}{...root}>{children}</div></>)}
ui/Popover.svelte
<scriptlang="ts"module>importtype{ Snippet }from'svelte'importtype{ HTMLAttributes }from'svelte/elements'exportinterfacePopoverPropsextendsOmit<HTMLAttributes<HTMLDivElement>,'popover'>{/** Whether the popover is open. Bind it, or leave it out and the trigger opens it. */open?:booleanonOpenChange?:(open:boolean)=>void/** The button that opens the popover: spread what it is given on a button of your own. */trigger?:Snippet<[{popovertarget:string}]>/** `auto` closes on a press outside and on Esc; `manual` stays until it is closed. */mode?:'auto'|'manual'}</script><scriptlang="ts">importtype{ Attachment }from'svelte/attachments'let{ open =$bindable(), onOpenChange, trigger, mode ='auto',class: className, id, children,...root }:PopoverProps=$props()const generated =$props.id()const name =$derived(id ?? generated)/** What the owner knows: the last state it set or was told. */let known =falseconstpresence:Attachment<HTMLDivElement>=(popover)=>{constreport=()=>{const shown = popover.matches(':popover-open')if(shown === known)return open = known = shownonOpenChange?.(known)} popover.addEventListener('toggle', report)$effect(()=>{if(open ===undefined)returnconst shown = popover.matches(':popover-open') known = openif(open &&!shown) popover.showPopover()elseif(!open && shown) popover.hidePopover()})return()=> popover.removeEventListener('toggle', report)}</script><!--@component A popover. The browser opens it from the trigger, places it against the trigger and dismisses it; this component only opens and closes it from `open`, and says when the browser did.-->{@rendertrigger?.({popovertarget: name })}<divid={name}popover={mode}class={['popover', className]}{@attach presence}{...root}>{@renderchildren?.()}</div>
ui/Popover.vue
<scriptsetuplang="ts">import{ computed, nextTick, onMounted, useId, useTemplateRef, watch }from'vue'defineOptions({inheritAttrs:false})const{ id, mode ='auto'}=defineProps<{id?:string/** `auto` closes on a press outside and on Esc; `manual` stays until it is closed. */mode?:'auto'|'manual'}>()/** Whether the popover is open: `v-model:open`. Leave it out and the trigger opens it. */const open =defineModel<boolean>('open')defineSlots<{/** The button that opens the popover: bind what it is given on a button of your own. */trigger?:(attributes:{popovertarget:string})=>unknowndefault?:()=>unknown}>()/* * A popover. The browser opens it from the trigger, places it against the trigger and dismisses it; this * component only opens and closes it from `open`, and says when the browser did. */const generated =useId()const name =computed(()=> id ?? generated)const root =useTemplateRef<HTMLDivElement>('root')/** What the owner knows: the last state it set or was told. */let known =falseconstsync=()=>{const popover = root.valueif(!popover || open.value===undefined)returnconst shown = popover.matches(':popover-open') known = open.valueif(open.value&&!shown) popover.showPopover()elseif(!open.value&& shown) popover.hidePopover()}constreport=()=>{const popover = root.valueif(!popover)returnconst shown = popover.matches(':popover-open')if(shown !== known) open.value= known = shown// An `open` this component is told wins: if its owner did not take the change, the popover goes back.voidnextTick(sync)}onMounted(sync)watch(open, sync,{flush:'post'})</script><template><slotname="trigger"v-bind="{popovertarget: name }" /><div:id="name":popover="mode"class="popover"ref="root"v-bind="$attrs"@toggle="report"><slot /></div></template>
The floating sheet, on an element with the popover attribute; without the attribute it is never shown. It opens below its button, lined up with the start edge, at a distance of --anchored-gap (6px), and flips at the viewport edge
popover-end
Lines up with the button's end edge, for a button at the end of a row. With popover
popover-center
Centers on the button. With popover
popover-top
Opens above the button; it combines with popover-end or popover-center. With popover
/* * Popover: a floating sheet opened by a button with popovertarget (or commandfor and command="toggle-popover"). * The popover attribute supplies the top layer, light dismiss and Escape; CSS anchor positioning places the * sheet against its button and flips it at the viewport edge. Geometry: a 12px corner, the same as a menu's, * and 16px of padding. */@layer components {.popover{--popover-padding:1rem;--popover-max-width:22rem;--surface-color:var(--color-surface-floating);width:max-content;max-width:min(var(--popover-max-width),calc(100vw-1rem));padding:var(--popover-padding);border:0;border-radius:calc(var(--radius-control-sm)*var(--corner-scale)+4px);corner-shape:var(--corner-shape);overflow:auto;background:var(--color-surface-floating);color:var(--color-text-body);box-shadow:var(--shadow-floating);font-size:.875rem;line-height:1.5;transform-origin:top;@apply--anchored;@apply--pop;scrollbar-width:thin;scrollbar-color:var(--color-line-strong)transparent;overflow-wrap: anywhere;overscroll-behavior:contain;/* Line up with the button's end edge instead of its start. */&.popover-end{--anchored-inline: span-inline-start;}&.popover-center{--anchored-inline: span-all;}/* Open above the button. */&.popover-top{--anchored-block:block-start;transform-origin:bottom;}@media(forced-colors:active){outline:1pxsolid CanvasText;outline-offset:-1px;}}}