29 Tailwind CSS popovers built on the native Popover API and CSS anchor positioning — for tooltips and hover cards, dropdown and profile menus, custom select listboxes, filter and date-picker panels, plus light-dismiss and manual modes, position-try collision flipping, animations, nested submenus, and dark-mode themes. Scoped under .tpop-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Building a whole Tailwind app, not just this component?
You just browsed 20 Tailwind popovers. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
The reference build the collection reuses: one button with popovertarget, one panel with popover, zero script. In v4 the open state is just the open: variant — it already matches :popover-open — so the panel's closed, open and entry states live in one class list, and the trigger's chevron flips via group-has-[#tpop-01-panel:popover-open]/root:rotate-180.
Two state machines, side by side, clickable. popover="auto" light-dismisses, answers Escape and closes other autos; popover="manual" ignores all three. The port adds what the original only described: a live status LED beside every invoker, lit by group-has-[#id:popover-open]/root:, so you watch opening Activity switch Notifications off.
Popover owns the top layer and dismissal; anchor positioning owns placement. In Tailwind both halves are arbitrary properties on the elements they describe — [anchor-name:--tpop03-a] on the chip, [position-anchor:--tpop03-a] [position-area:block-end_span-inline-end] on the panel — and the UA reset that makes it work sits behind supports-[anchor-name:--a]: so unanchored engines still get a centred panel.
The three-part recipe almost everyone ships two-thirds of, as utilities: starting:open: for the entry values, transition-discrete so display animates, and overlay in the transition list so the panel stays in the top layer while it leaves. The port adds an asymmetric timing — 260ms in, 140ms out — with nothing more than duration-140 open:duration-260.
Every popover gets a ::backdrop, and in Tailwind it is just the backdrop: variant — stack it as open:backdrop: and starting:open:backdrop: and the page dim fades with the panel. Light dismiss stays intact: the backdrop decorates, it does not shield, so a click on the dimmed page still closes an auto popover. The manual twin has the same dim and none of the dismissal.
position-area treats the space around an anchor as a 3×3 grid and lets you name a cell. This board makes it literal: eight buttons around one anchor, each opening a panel in its own cell — and in the port, each panel's placement is one arbitrary property you can read off the markup: [position-area:block-start_inline-start]. The panel also prints its physical equivalent, and the cell that opened it lights up while it's open.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Collision handling was the main reason to install a positioning library. [position-try-fallbacks:flip-block,flip-inline,--tpop07-side] replaces it: list what you accept and the browser tries each until the panel fits. Drag the strip to push the node to an edge and watch the panel change sides. The one thing that stays CSS is the named @position-try block — an at-rule has no utility.
The bubble is easy; the tail is what breaks. Here the tail is the bubble's after: pseudo-element, itself anchor-positioned to the trigger with the same area and the same flip list, so it stays on the button's midline when the bubble is clamped or flipped. Every rule of it is an after: utility — gated so unanchored engines draw no tail rather than a wrong one.
A panel exactly as wide as the control that opened it reads as part of that control. supports-[anchor-name:--a]:w-[anchor-size(width)] sizes the panel from its anchor live, with no offsetWidth — and because the variant is emitted after the base w-[min(23rem,…)], the fallback width is declared first for free. Resize the frame and the seam never opens.
Two auto popovers can be open at once only if the browser considers one nested in the other — by DOM containment or by the popovertarget relationship. This menu keeps the submenu inside the menu, anchors each level to its own trigger and flips the submenu inward at the edge. The port also fixes a real bug: the original set display: grid on both panels, which overrides the UA's display: none and leaves them permanently visible — here the grid is open:grid.
Popover supplies the container — top layer, light dismiss, Escape, focus return — and nothing about listbox semantics. Thirty-odd lines of vanilla JS add roving tabindex, arrow/Home/End/Enter and now typeahead; everything visual reads the ARIA state directly: aria-selected:font-semibold aria-selected:text-tpop11-sel on options and aria-expanded:border-tpop11-sel on the trigger. The script never touches a class.
Popover's built-in trigger is click, so hover and focus call showPopover()/hidePopover() themselves: a 150ms open delay for hover intent, instant close, instant open on keyboard focus. The bubble is popover="manual" so it never fights the auto stack. The port keeps the controller to twenty lines and moves every visual — the dotted term, the rise-in, the definition card — into utilities.
The canonical app-header pattern with no script: an avatar anchors an account menu that opens toward the page interior, flips at the edge, and — because it lives in the top layer — ignores the header's overflow-hidden and needs no z-index. The port adds a real menu entrance: it scales out of the avatar's corner with origin-top-right, and the avatar gains a ring while its menu is open.
A calendar tethered to a date field — the field wrapper is the anchor, the icon button is only the invoker — with the month as a seven-column grid of tabular-nums cells. No date logic; the lesson is tethering and legibility at 320px. The port fixes the original's invalid role="grid" (grids require rows and gridcells), and marks today with a dot and the selection with fill and weight.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
The clearest real use of popover="manual": a filter menu you tick several boxes in, which must not vanish because a click landed just outside it. The port makes the panel live with zero JS — a CSS counter ([counter-reset:tpop15] + checked:[counter-increment:tpop15]) prints how many filters are ticked, the chip shows a dot whenever any are, and a native type="reset" restores the defaults.
Popover and anchor positioning ship on different timelines, so one test isn't enough. The script detects three features — popover, anchor positioning and discrete transitions — writes each to a data-* attribute on the root, and every visual reads them through group-data-[anchor=yes]/root: variants. The panel layers up: a plain positioned box with no API, popover dismissal on top, anchoring on top of that.
One decision rule, shown twice: <dialog> via command="show-modal" when the user must deal with it, popover when they can ignore it and carry on. The port animates both with the same utilities — open: matches [open] on the dialog and :popover-open on the panel — so the comparison isolates modality, and fixes the preflight trap that left the dialog pinned to the top-left.
A frosted panel over a rich aurora, with the opaque fallback as the base class and the frost layered on with supports-[backdrop-filter:blur(1px)]: — so an unsupporting engine shows a solid card, never text over raw colour. Because the panel lives in the top layer it has no transformed ancestor to become its backdrop root: the blur samples the real page.
One number drives the whole theme. @theme inline defines every colour as color-mix(light, dark, var(--tpop19-d)); the root sets --tpop19-d to 0 or 1 with four utilities — dark:, has-checked: and dark:has-checked: for the flip-back — and because --tpop19-d is a registered @property, the root can transition it: the page and the open popover crossfade between palettes in the same frame, zero JS.
A ledger of what the API gives you — top layer, light dismiss, Escape, focus return — and what it doesn't: a role, a name, a described-by relationship. The demo wires both correctly: a click-opened hint with aria-describedby and no tooltip role, and a role="menu" panel. The port fixes the original's always-visible menu (display: grid outranked the UA's closed state) with open:grid.
FAQ
Frequently asked questions
How does the Popover API show and hide a panel with no JavaScript?
You need exactly two attributes and one shared id. Put popovertarget="menu" on the trigger <button> and popover plus id="menu" on the panel. The browser wires the click for you — it promotes the panel to the top layer, toggles it, and returns focus to the trigger on close. In Tailwind the panel styling is ordinary utilities; the only popover-specific piece is the open: variant, which matches while the panel is shown, so open:opacity-100 and its closed default give you a visible transition. The common first bug is styling the panel as if it were a normal absolutely-positioned <div>: once it enters the top layer it ignores its old stacking context and any overflow-hidden ancestor, so paint it with its own background and shadow rather than relying on the parent. No script, no z-index war.
What is the difference between popover=auto, popover=manual, and how do I stop light dismiss?
popover="auto" is the default behaviour Demo 02 shows: it light-dismisses, so clicking the backdrop or pressing Escape closes it, and opening a second auto popover closes the first unless one is nested inside the other. That is wrong for a filter menu where you tick several boxes in a row — one stray outside click wipes the selection. Switch that panel to popover="manual" (Demo 15): it ignores outside clicks and Escape entirely, and you own dismissal with a real close <button> calling hidePopover(). Two auto popovers can only coexist through DOM containment (Demo 10's submenu). The trap: authors reach for manual to keep a tooltip open, then forget to give the user any way out — always ship an explicit close control and, for menus, still handle Escape yourself in script since manual won't.
How do I position a popover next to its trigger with CSS anchor positioning?
The Popover API only controls the top layer and dismissal — it says nothing about placement, so Demo 03 pairs it with CSS anchor positioning. Give the trigger anchor-name and the panel position-anchor pointing at that name, then place it. Two placement styles appear: position-area (Demo 06) treats the anchor's surroundings as a 3×3 grid so you name a cell like block-end/inline-start, and raw anchor() insets for pixel control. In Tailwind every one of these is an arbitrary property, e.g. [position-area:block-end]. The frequent mistake is expecting the panel to move with the anchor without the name link — if position-anchor doesn't match the trigger's anchor-name string exactly, the panel falls back to normal flow and lands top-left. Anchor names are case-sensitive dashed idents, not selectors.
Why does my popover open and close instantly with no animation, and how do I fix the exit?
Transitioning a popover needs three pieces, and Demo 04 exists because almost everyone ships two. First the entry: the panel animates from display:none, so the start values live in the starting:open: variant. Second the exit: an element leaving the top layer is removed from the render tree immediately unless you transition display and overlay and mark them allow-discrete — miss this and close is instant while open animates fine, the classic asymmetry. Third, put overlay in the transition list so the top-layer promotion itself is deferred until the animation ends. In Tailwind: transition-discrete plus starting:open:opacity-0 and the open-state target. Also honour motion-reduce: to drop the movement. If only the exit is broken, you forgot allow-discrete on display.
How do I draw a backdrop behind a popover and make clicking it close the panel?
Every popover automatically gets a ::backdrop pseudo-element in the top layer, and in Tailwind you reach it with the backdrop: variant — backdrop:bg-black/40 backdrop:backdrop-blur-sm tints and blurs everything behind the panel (Demo 05). You do not add a separate overlay <div>; that would sit in the wrong layer and need its own click handler. Light dismiss is already wired for popover="auto", so a click on that backdrop closes the panel for free — no listener. Two gotchas: the backdrop only exists while the popover is open, so transition it with the same starting: and allow-discrete discipline as the panel or it pops in hard; and a manual popover still renders a backdrop but will not dismiss on click, so if you tint one you must give users a close button, or the dimmed page reads as frozen.
The Popover API only opens on click — how do I make a tooltip show on hover and focus accessibly?
The built-in invoker is a click, which is wrong for a tooltip. Demo 12 keeps the top-layer and dismissal benefits but drives visibility itself: call showPopover() on mouseenter and focus, hidePopover() on mouseleave and blur. Focus is not optional — a hover-only tooltip is invisible to keyboard and many screen-reader users, so both triggers are required, and Escape must still dismiss. For the semantics, Demo 20 is the ledger: the API gives you the top layer, light dismiss, Escape and focus return, but it does not give you a role. Add role="tooltip" to the bubble and point the trigger at it with aria-describedby so the description is announced. The common bug is a tooltip that traps or steals focus — a tooltip should never be focusable itself; only its trigger is in the tab order.
When should I use a popover versus a <dialog> modal?
Demo 17 reduces it to one rule. Use a modal <dialog> opened with command="show-modal" when the task must be finished or cancelled before anything else — it is modal, so it traps focus, makes the rest of the page inert, and demands a decision. Use a popover when the surface is supplementary and the user should be able to keep working around it: menus, tooltips, dropdowns, date pickers, filter panels. The practical tell is focus: a popover does not trap focus or inert the background, so if you build a login or a destructive confirmation as a popover, a Tab or an outside click walks the user straight past the decision. Conversely, forcing a tooltip into a modal dialog is hostile — it blocks the page for a hint. Match modality to consequence: irreversible or blocking, use the dialog; informational or navigational, use the popover.
Popover and anchor positioning aren't supported everywhere — how do I add a fallback?
They ship on different timelines, so one capability test isn't enough — Demo 16 checks two things separately. Feature-detect the Popover API with HTMLElement.prototype.hasOwnProperty('popover') and anchor positioning with CSS.supports('anchor-name: --x'); a browser can have the first without the second. When popover is missing, load the official polyfill; when only anchor positioning is missing, keep the popover but fall back to a plain absolute-positioned placement so the panel still appears in a sane spot. Demo 09 shows the CSS-side guard with the supports-[anchor-name]: variant so anchored width only applies where it works. Demo 18's glassmorphism uses the same idea for visuals: the opaque panel is the base class and the frosted backdrop-blur is layered on top, so a browser without backdrop-filter still gets a readable panel instead of transparent unreadable text.