Popover
The <x-wirekit:: component creates a click-triggered floating panel anchored to a trigger element. Unlike a Tooltip (hover, plain text) or Hover Card (hover, rich content), the Popover is activated on click and is ideal for small forms, share menus, or quick settings.
Usage
Compact URL Copy
For a space-efficient share popover, place the copy icon directly inside the input:
Use padding-right on the input to reserve space so text stops before the icon area. icon-only drops the Copied! label and its reserved width, so the control stays square inside the field — the confirmation still reaches a screen reader through the component's own live region. For long URLs, add a gradient fade overlay between the text and the icon button.
Placement
Control where the panel appears relative to the trigger:
<x-wirekit::popover placement="bottom-start">
<x-slot:trigger>
<x-wirekit::button>Options</x-wirekit::button>
</x-slot:trigger>
Panel content
</x-wirekit::popover>
Available placements: top, top-start, top-end, bottom (default), bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end.
The panel automatically flips to the opposite side when there is not enough space and shifts to stay within the viewport, powered by Floating UI.
With Form Content
Popovers are well suited for inline forms that don't warrant a full modal. Put the <form> inside the popover, as below: the panel is moved to the end of the page so no ancestor can clip it, and a form around the popover no longer contains the fields in it.
<x-wirekit::popover placement="bottom-end">
<x-slot:trigger>
<x-wirekit::button intent="neutral" surface="outline" size="sm">Rename</x-wirekit::button>
</x-slot:trigger>
<form wire:submit="rename" class="flex flex-col gap-3" style="width: 18rem;">
<x-wirekit::input name="new-name" label="New name" />
<x-wirekit::button type="submit" size="sm">Save</x-wirekit::button>
</form>
</x-wirekit::popover>
Popover vs Tooltip vs Hover Card vs Dropdown
| Feature | Tooltip | Hover Card | Popover | Dropdown |
|---|---|---|---|---|
| Trigger | Hover / Focus | Hover / Focus | Click | Click |
| Content | Plain text | Rich HTML (read-only) | Interactive (forms, buttons) | Menu items / actions |
| ARIA role | tooltip |
dialog |
dialog |
menu |
| Focus trapped | No | No | Yes | No (roving tabindex) |
| Use case | Labels, abbreviations | User profiles, previews | Inline forms, share menus | Action lists |
Width & Layout
Popover width is determined by its content. To set an explicit width, apply Tailwind classes to the panel slot content:
<x-wirekit::popover>
<x-slot:trigger>…</x-slot:trigger>
<div class="w-72 p-4">Fixed-width popover content.</div>
</x-wirekit::popover>
Behavior
- Floating UI (bundled) handles positioning with flip and shift middleware
- Focus trap (bundled) keeps keyboard navigation within the panel
- Click outside closes the popover automatically. A click inside the panel, on a field, a label or the panel itself, leaves it open
- Livewire SPA navigation (
wire:navigate) automatically closes open popovers - Transitions use scale + opacity for smooth open/close animation
Props
| Prop | Type | Default | Description |
|---|---|---|---|
placement |
string |
'bottom' |
Floating UI placement |
offset |
int |
8 |
Distance in px between trigger and panel |
label |
string|null |
null |
The panel's accessible name. It is a role="dialog", so a screen reader announces this on entry; without it the announcement is the generic word "Popover", which is the same for every popover on the page. |
padded |
bool |
true |
Whether the panel pads its own contents. Pass false for a panel that brings its own header, scroll region or footer — those have to reach the panel's edges, and outer padding puts a gutter between the scrollbar and the border. |
scope |
string|null |
null |
Scoped personalization key |
Slots
| Slot | Purpose |
|---|---|
trigger |
The element that toggles the popover on click |
| default | Interactive content inside the floating panel |
Accessibility
- An
aria-labeloraria-labelledbyof yours names the panel in place oflabel - Trigger:
aria-haspopup="dialog"-- announces that activation opens a dialog-like panel - Trigger:
aria-expanded-- toggles between"true"and"false" - Panel:
role="dialog"-- screen readers announce content as a dialog - Panel:
aria-modal="true"-- the panel traps focus and closes on Escape, so assistive technology is told the page behind it cannot be reached while it is open - Focus trap active when open -- Tab cycles within the panel only
- Focus returns to the trigger when the popover closes from inside: Escape, a control in the panel that closes it, or the trigger. A click outside leaves focus on what was clicked
- Click outside closes the popover; a click inside the panel leaves it open
- Escape closes the popover
- SPA-safe: cleans up on Livewire page navigation (
wire:navigate)
Keyboard Interaction
| Key | Action |
|---|---|
Enter / Space (on trigger) |
Toggle the popover |
Tab / Shift+Tab (inside the open popover) |
Cycle focus through interactive content (focus trap) |
Escape |
Close the popover and return focus to the trigger |
Pitfalls
- Don't use a popover where a tooltip would do. Tooltips describe; popovers contain interactive content. The wrong choice misses focus management.
- Don't auto-open a popover on page load. It steals focus and confuses screen-reader users — popovers are user-triggered by design.
Design Tokens
| Element | Token |
|---|---|
| Panel background | --color-wk-bg-elevated |
| Panel border | --color-wk-border / --border-wk-width |
| Panel radius | --radius-wk-lg |
| Panel shadow | --shadow-wk-lg |
| Panel padding | --padding-wk-x-md |
| Text color | --color-wk-text |
| Text size | --text-wk-md |
| Font family | --font-wk-sans |
| Transition | --transition-wk-duration |
Personalization
Override defaults in config/:
'components' => [
'popover' => [
'placement' => 'bottom-start',
'offset' => 12,
],
],
Scoped Personalization
Apply different styles to specific instances using the scope prop:
<x-wirekit::popover scope="share-menu">
...
</x-wirekit::popover>
// In a service provider's boot() method. A closure receives the shipped classes, so the
// component keeps them; a plain string would replace them outright.
WireKit::scope('share-menu', [
'popover' => ['classes' => [
'panel' => fn (string $vendor): string => str_replace('min-w-72', 'min-w-56', $vendor),
]],
]);
Further Reading
- WAI-ARIA Dialog (Non-Modal) Pattern -- popover is a non-modal dialog variant
- Floating UI -- positioning engine (bundled)
- focus-trap -- focus management library (bundled)
- MDN:
role="dialog" - MDN:
aria-haspopup