---
title: Popover
description: Contextual content popover
visibility: guest
draft: false
---

# Popover

The `<x-wirekit::popover>` component creates a click-triggered floating panel anchored to a trigger element. Unlike a [Tooltip](/components/tooltip) (hover, plain text) or [Hover Card](/components/hover-card) (hover, rich content), the Popover is activated on click and is ideal for small forms, share menus, or quick settings.

## Usage

<!-- markdownlint-disable MD034 -->

:::preview{title="Share Options Popover"}
<x-wirekit::popover>
    <x-slot:trigger>
        <x-wirekit::button intent="neutral" surface="outline" size="sm">Share</x-wirekit::button>
    </x-slot:trigger>
    <x-wirekit::stack gap="sm">
        <x-wirekit::input aria-label="Share link" name="share-link" value="https://wirekit.app/docs" readonly />
        <x-wirekit::clipboard-button value="https://wirekit.app/docs" style="width: 100%; justify-content: center;">
            Copy Link
        </x-wirekit::clipboard-button>
    </x-wirekit::stack>
</x-wirekit::popover>
:::

<!-- markdownlint-enable MD034 -->

### Compact URL Copy

For a space-efficient share popover, place the copy icon directly inside the input:

:::preview{title="Compact URL Copy"}
<x-wirekit::popover placement="bottom-start">
    <x-slot:trigger>
        <x-wirekit::button intent="neutral" surface="outline" size="sm">Share</x-wirekit::button>
    </x-slot:trigger>
    <div style="position: relative; width: 22rem;">
        <x-wirekit::input
            aria-label="Share URL"
            name="share-url"
            value="https://wirekit.app/docs/components/popover"
            readonly
            style="padding-right: 2.25rem;"
        />
        {{-- Icon-only copy affordance. <x-wirekit::clipboard-button> always
             reserves layout space for its 'Copied!' label (stable-width
             pattern) so it would render too wide here; a hand-rolled inline
             Alpine button keeps the trigger square without that overhead. --}}
        <button
            type="button"
            x-data="{ copied: false }"
            x-on:click="
                navigator.clipboard.writeText('https://wirekit.app/docs/components/popover');
                copied = true;
                setTimeout(() => { copied = false; }, 1500);
            "
            aria-label="Copy URL to clipboard"
            :aria-label="copied ? 'Copied to clipboard' : 'Copy URL to clipboard'"
            style="position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%); display: inline-flex; align-items: center; justify-content: center; height: 1.75rem; width: 1.75rem; padding: 0; border: 0; background: transparent; color: var(--color-wk-text-muted); border-radius: var(--radius-wk-sm); cursor: pointer;"
        >
            <svg x-show="!copied" style="width: 1rem; height: 1rem;" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
                <path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
            </svg>
            <svg x-show="copied" style="width: 1rem; height: 1rem; color: var(--color-wk-success-text);" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true" x-cloak>
                <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
            </svg>
            <x-wirekit::visually-hidden x-show="copied" role="status" aria-live="polite">Copied to clipboard</x-wirekit::visually-hidden>
        </button>
    </div>
</x-wirekit::popover>
:::

Use `padding-right` on the input to reserve space so text stops before the icon area. The icon button is hand-rolled for the icon-only embedded case — `<x-wirekit::clipboard-button>` always reserves layout for its `Copied!` label (a stable-width feature for labeled use) which would render too wide inside an input. 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:

:::preview{title="Popover Placement Options"}
<x-wirekit::row wrap gap="md" justify="center" style="min-height: 12rem; padding: 2rem;">
<x-wirekit::popover placement="top">
    <x-slot:trigger>
        <x-wirekit::button intent="neutral" surface="outline" size="sm">Top</x-wirekit::button>
    </x-slot:trigger>
    Opens above
</x-wirekit::popover>
<x-wirekit::popover placement="bottom">
    <x-slot:trigger>
        <x-wirekit::button intent="neutral" surface="outline" size="sm">Bottom</x-wirekit::button>
    </x-slot:trigger>
    Opens below
</x-wirekit::popover>
<x-wirekit::popover placement="left">
    <x-slot:trigger>
        <x-wirekit::button intent="neutral" surface="outline" size="sm">Left</x-wirekit::button>
    </x-slot:trigger>
    Opens left
</x-wirekit::popover>
<x-wirekit::popover placement="right">
    <x-slot:trigger>
        <x-wirekit::button intent="neutral" surface="outline" size="sm">Right</x-wirekit::button>
    </x-slot:trigger>
    Opens right
</x-wirekit::popover>
</x-wirekit::row>
:::

```blade
<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](https://floating-ui.com/).

## With Form Content

Popovers are well suited for inline forms that don't warrant a full modal:

:::preview{title="Inline Rename Form"}
<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>
    <x-wirekit::stack gap="sm" style="width: 18rem">
        <x-wirekit::input name="new-name" label="New name" value="project-alpha" />
        <x-wirekit::button size="sm">Save</x-wirekit::button>
    </x-wirekit::stack>
</x-wirekit::popover>
:::

```blade
<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:

```blade
<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](https://floating-ui.com/)** (bundled ~3.5 KB) handles positioning with [flip](https://floating-ui.com/docs/flip) and [shift](https://floating-ui.com/docs/shift) middleware
- **[Focus trap](https://github.com/focus-trap/focus-trap)** (bundled ~3.8 KB) keeps keyboard navigation within the panel
- **Click outside** closes the popover automatically
- **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](https://floating-ui.com/docs/computePosition#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

- Trigger: `aria-haspopup="dialog"` -- announces that activation opens a dialog-like panel
- Trigger: `aria-expanded` -- toggles between `"true"` and `"false"`
- Trigger: `aria-controls` -- linked to the panel's unique `id` (auto-generated)
- Panel: `role="dialog"` -- screen readers announce content as a dialog
- Panel: `aria-modal="false"` -- the popover does not block the rest of the page semantically (unlike a modal)
- **Focus trap** active when open -- Tab cycles within the panel only
- **Focus returns** to the trigger element on close
- **Click outside** closes the popover
- **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/wirekit.php`:

```php
'components' => [
    'popover' => [
        'placement' => 'bottom-start',
        'offset' => 12,
    ],
],
```

### Scoped Personalization

Apply different styles to specific instances using the `scope` prop:

```blade
<x-wirekit::popover scope="share-menu">
    ...
</x-wirekit::popover>
```

```php
// config/wirekit.php
'personalizations' => [
    'popover' => [
        'share-menu' => [
            'base' => 'max-w-sm',
        ],
    ],
],
```

## Further Reading

- [WAI-ARIA Dialog (Non-Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) -- popover is a non-modal dialog variant
- [Floating UI](https://floating-ui.com/) -- positioning engine (bundled, ~3.5 KB)
- [focus-trap](https://github.com/focus-trap/focus-trap) -- focus management library (bundled, ~3.8 KB)
- [MDN: `role="dialog"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role)
- [MDN: `aria-haspopup`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-haspopup)
