UIDex

Popover

اللوحة المنبثقة

popover="auto" + role="dialog"

also calledpopup panel، floating panel، anchored overlay، flyout، popover panel، بوب أوفر، بوب أب، صندوق منبثق، نافذة طافية، لوحة عائمة، فقاعة منبثقة

A popover is a small layer that opens when you press a trigger, sits anchored to it, and carries real interactive content: fields, buttons, switches. It is non-modal, so the page underneath stays clickable, focus is not trapped inside it, and an outside click or the Escape key closes it. Four different things collect the same description, "a little box that pops up", and two questions separate them: what opens it, and whether it blocks what is underneath. A tooltip opens on hover or on keyboard focus, holds text that describes the element it is tied to, and must never carry a button, because the keyboard has no route into it. A menu is a row of commands where picking one closes it, arrow keys move through it rather than Tab, and it carries role="menu" with role="menuitem" instead of role="dialog". A modal covers the page under it and traps focus, declares as much with aria-modal="true", and does not go away without a decision. A hover card holds content the way a popover does, but the user never meant to open it, so nothing that exists only there belongs inside one, and a touch screen has no hover at all.

If you called it…

"the little box that pops up when I click a button""the panel with a small arrow pointing at the button""like a tooltip but you can click inside it""the popup that closes when you click somewhere else""the small card with the filters in it"

Live specimen

Interact with the demo. Every part is real and numbered.

٤٢ نتيجة
اللوحة تتّجه حيث توجد المساحة
RTLspan-inline-start يمتدّ يميناً في العربية ويساراً في الإنجليزية

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a non-modal popover. Make the trigger a real <button> carrying an aria-expanded that flips on every open and close, and give it a popovertarget pointing at the panel id. Make the panel an element with popover="auto" so light dismiss by outside click and Escape works with no JavaScript, give it role="dialog" with aria-labelledby pointing at its heading, and never write aria-modal="true" or trap focus inside it. Move focus in only when the panel holds something focusable, and use autofocus on the first field when it holds a form. Add an explicit close button with popovertargetaction="hide" and an accessible name, and return focus to the trigger after every close. Pin it with anchor positioning: anchor-name on the trigger, position-anchor on the panel, position-area: block-end span-inline-start, and position-try-fallbacks: flip-block, flip-inline so it flips at the viewport edge, then add a @position-try rule of your own if it also has to slide along its axis, because CSS ships no shift tactic. Make the arrow absolutely positioned with the same position-anchor, tie it down with justify-self: anchor-center, and mark it aria-hidden="true". Style the open state through :popover-open, add @starting-style for the entry frame, and put transition-behavior: allow-discrete on display and overlay so the exit animation gets to run. Write every inline-axis measurement as a logical property: inset-inline-start, margin-inline, padding-inline, border-inline, and never left, right, or margin-left anywhere, so the whole panel mirrors when the page flips to RTL. If you use a positioning library instead of CSS, keep the arrow offset it returns on left because that number is physical, and call autoUpdate so the panel follows its trigger through scrolling and resizing.

How it behaves right-to-left

This section is ours alone.

Placement keywords: logical vs physical

mirrors

position-area takes physical keywords (top, left, right, bottom) and logical ones (block-start, inline-start, inline-end), and only the logical set is resolved against direction. position-area: block-end span-inline-start puts the panel below the trigger extending toward the left in English and toward the right in Arabic, while position-area: bottom left stays on the left in both. Positioning libraries split the same way: Floating UI resolves the -start and -end suffixes from the direction it computes on the floating element itself, so bottom-start aligns to the right edge on an RTL page, while the base side (top, bottom, left, right) never mirrors however the page flips.

The arrow offset

mirrors

A positioning library hands you the arrow offset as a physical number: middlewareData.arrow.x in Floating UI is measured from the left edge of the panel whatever the page direction. Feed it to inset-inline-start and the browser measures from the right under [dir="rtl"], so the arrow lands at the opposite end; assign it to left exactly as it arrives and keep inset-inline-start for offsets you author yourself. Pure CSS drops the arithmetic: an absolutely positioned arrow sharing the panel position-anchor takes justify-self: anchor-center, centres itself on the anchor, and never asks which side is which.

A portalled panel loses dir

mirrors

A native popover stays where it is in the DOM and is only painted in the top layer, so it inherits dir like anything else. createPortal into document.body takes it out of the Arabic subtree: if dir="rtl" is written on a <div id="app"> instead of on <html>, the portalled panel inherits LTR, Arabic text goes flush left, trailing punctuation lands at the wrong end, and every logical property inside resolves backwards. Put dir on <html>, or set it explicitly on the portal root when you create it, because Floating UI reads that same LTR direction off the panel and aligns bottom-start to the wrong edge on top of everything else.

Flipping at the viewport edge

mirrors

Which edge runs out of room changes with direction: a panel placed with span-inline-start extends to the left in English and to the right in Arabic, so a trigger that never flipped in English starts flipping in Arabic. flip-inline inside position-try-fallbacks works on the inline axis and follows direction by itself, while a hand-rolled check such as if (rect.right > innerWidth) tests one physical edge and never sees the overflow on the other side. The measurement flips too: in an RTL horizontal scroller current browsers report scrollLeft as 0 at the far right, going negative as the user scrolls left, so a formula like trigger.offsetLeft - scroller.scrollLeft pushes the panel the wrong way as long as it assumes a positive value.

The block axis does not mirror

never mirrors

direction acts on the inline axis only, so a panel that opens below its trigger still opens below it in Arabic, only writing-mode would move it, and block-end in position-area needs no [dir="rtl"] exception. A blanket rule such as [dir="rtl"] .popover { transform: scaleX(-1) } mirrors the text and the icons along with the box and leaves the whole panel drawn backwards. One thing does need a hand-written override: transform-origin accepts no logical keyword on either axis, so a scale-in written as transform-origin: top left has to be restated as top right under [dir="rtl"] while the vertical half stays top.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

HTMLpopover="auto" + popovertargetA native pairing that gives you the top layer and light dismiss with no JavaScript at all.
CSS:popover-openThe one hook for the open state, since the panel is display: none the rest of the time.
CSSanchor-name + position-anchorTies the panel to its trigger with no positioning library and no JavaScript maths.
CSSposition-try-fallbacks: flip-block, flip-inlineThe flip at the viewport edge, whose inline half follows page direction on its own.
ARIArole="dialog" without aria-modalThe role names the layer; aria-modal here is a claim that hides the rest of the page.
Radix UI<Popover.Root modal={false}>false is the default, so no focus trap, and <Popover.Anchor> separates the anchor from the trigger.
Floating UIflip() + shift() + arrow()flip and shift keep the panel on screen, arrow places the tip, and autoUpdate follows the trigger while you scroll.

See also

Updated · 2026-08-19