0.1.0

Popover

Popovers anchor rich content to a trigger element (filters, mini forms, or contextual details) without modal focus trapping.

Try it

Usage

Wrap a child RuiPopover in RuiPopoverTrigger. Control open, placement, and portal on the trigger or popover.

import { RuiPopover, RuiPopoverTrigger, RuiPopoverContent } from '@ecopages/radiant-ui/popover';
import { RuiButton } from '@ecopages/radiant-ui/button';
 
<RuiPopoverTrigger trigger={<RuiButton variant="outline">Filter</RuiButton>}>
  <RuiPopover placement="bottom-start">
    <RuiPopoverContent>
      <p>Show items from the last 7 days.</p>
    </RuiPopoverContent>
  </RuiPopover>
</RuiPopoverTrigger>

Custom markup

<rui-popover> coordinates any light-DOM tree that matches its query contract. Pair with <rui-popover-trigger> for click-to-toggle, or pass anchor for an external selector.

import '@ecopages/radiant-ui/popover';
 
<rui-popover-trigger>
  <div data-ref="root" class="rui-popover-trigger">
    <span data-popover-trigger>
      <button type="button">Filter</button>
    </span>
    <rui-popover placement="bottom-start">
      <div data-ref="host" class="rui-popover-host">
        <div data-ref="surface" class="rui-popover rui-floating" role="dialog">
          <p>Show items from the last 7 days.</p>
        </div>
      </div>
    </rui-popover>
  </div>
</rui-popover-trigger>;

For a standalone popover anchored elsewhere, omit rui-popover-trigger and set anchor="#my-button" on <rui-popover>.

Listbox variant

Set variant="listbox" when the popover hosts a RuiListbox; width and focus behavior adapt automatically.

Match anchor width

Enable matchAnchorWidth for select-style popovers so the panel aligns with the trigger.

Theming

The popover surface maps to semantic surface roles, never Tailwind palette steps. The panel is often portaled; override --rui-popover-* on .rui-popover.

PartDefaultOverride
Fill / border / radius / shadow--background, --border, --radius-container, --shadow-overlay--rui-popover-surface, --rui-popover-border-color, --rui-popover-radius, --rui-popover-shadow
Listbox variantStripped padding for embedded listboxesclass rui-popover--listbox
Z-orderpopovertheme z-index
.rui-popover {
  --rui-popover-radius: var(--radius-control);
}

Accessibility

  • Triggers expose aria-expanded when the popover is open.
  • Content is focusable and dismissible with Escape.
  • Do not trap focus; users should reach surrounding content without closing first.

API

RuiPopover is a custom element (<rui-popover>) positioning a floating role="dialog" surface. RuiPopoverTrigger (<rui-popover-trigger>) coordinates open state with the trigger click.

Attributes (<rui-popover>)

AttributeTypeDefaultDescription
openbooleanfalseWhether the popover is open (controlled).
placementRuiPlacementbottomPlacement relative to the anchor.
portalbooleantrueTeleport the surface to document.body.
match-anchor-widthbooleanfalseMatch the anchor width (dropdown menus).
offsetnumber8Gap between anchor and surface in px.
anchorstring''CSS selector for an external anchor.
variantdefault · listboxdefaultSurface variant; listbox strips padding.

Attributes (<rui-popover-trigger>)

AttributeTypeDefaultDescription
openbooleanfalseWhether the popover starts open.

Light-DOM contract (<rui-popover>)

TargetRequiredHost writesAuthor owns
[data-ref="host"]yes—anchor + surface wrapper
[data-ref="surface"]yesid, portal positionfloating panel content
[data-popover-trigger]yes*aria-controls, aria-expanded on anchorpressable anchor wrapper

* Or use the anchor attribute with a CSS selector instead of [data-popover-trigger].

Nested hosts: rui-popover-trigger (parent provides the trigger target).

Light-DOM contract (<rui-popover-trigger>)

TargetRequiredHost writesAuthor owns
[data-ref="root"]yes—click delegate root
[data-popover-trigger]yes—pressable anchor
rui-popoveryessyncs open both wayschild popover element

Events

EventDetailDescription
rui-open-change{ open: boolean }Emitted when open state changes.

Methods (<rui-popover>)

MethodDescription
setOpen(next, emit?)Toggle open state. Pass emit = false to sync without firing rui-open-change.

View helpers

ComponentTarget stampedNotes
RuiPopoverTrigger[data-ref="root"], [data-popover-trigger]Wraps child rui-popover. Requires trigger prop.
RuiPopover[data-ref="host"], [data-ref="surface"], optional [data-popover-trigger]trigger prop for inline anchor; children are surface content.
RuiPopoverContent—Body inside [data-ref="surface"]; not a query target.

CSS classes

Public BEM classes (documented via @cssclass):

ClassDescription
.rui-popover-hostAnchor + surface wrapper.
.rui-popoverFloating surface (role="dialog"); background + rounded-container + shadow-overlay.
.rui-popover--listboxStripped padding for embedded listboxes.
.rui-popover-triggerTrigger + popover wrapper.

Theme roles

PartCSS variables consumed
Surface--background, --on-background, --border, --radius-container, --shadow-overlay
Z-order--z-popover