<c-popover>
A click-opened, non-modal surface anchored to its trigger, floating in the top layer; its content may be interactive.
Usage
Put the element that opens the popover in the trigger slot and the content in the default slot. An optional heading prop renders a heading and doubles as the panel's accessible name (it is not called title — a title attribute would trigger the browser's native tooltip). The popover is deliberately minimal: compose footers, actions, or a close button from existing components (e.g. c-button) inside the body.
Designated trigger
When the opener cannot be slotted — a button living elsewhere in the layout — point the trigger prop at it instead: the ID of the element, or the element itself via property binding. The two routes are the same concept: the popover wires click-to-toggle onto the designated element, mirrors aria-haspopup/aria-expanded, returns focus to it on Escape, anchors the panel to it, and clicking it never counts as a light dismiss. Supplying both routes is a misuse: the prop wins, the slotted element gets no wiring, and a console warning flags it.
When to use
- Contextual, dismissable UI attached to a control: a small settings panel, a confirm-lite prompt, help content with links.
- Not for a plain text hint — use
c-tooltip. - Not for blocking flows the user must complete — the popover never traps focus or blocks the page; use
c-modal.
Dismissal
Clicking outside the popover or pressing Escape closes it. Escape returns focus to the trigger when focus was inside the panel; light dismiss leaves focus where the user clicked. Focus is not moved into the panel on open — Tab reaches the content naturally from the trigger.
Nesting
Popovers nest: a control inside one popover's panel can open another. The open popovers form a popover chain — each nested in the previous one; opening a popover whose trigger is not inside the open one closes it first, so sibling popovers never coexist. Escape closes only the innermost popover, one press per layer; clicking outside closes every popover that does not contain the click (clicking in a parent panel closes just its child); closing a popover closes everything nested inside it. Nesting follows the trigger, not the markup — a popover opened from a designated trigger inside another popover's panel chains under it even though its host element lives elsewhere.
Accessibility
The panel is a non-modal role="dialog" and needs an accessible name: the heading prop provides one, or set aria-label on the c-popover element (aria-labelledby cannot reference slotted content across the shadow boundary). The component warns in the console when it opens unnamed. aria-haspopup="dialog" and aria-expanded are mirrored onto the slotted trigger.
Layering
The panel is a native popover in the top layer: it is never clipped by ancestor overflow and needs no z-index management, even when opened from inside a modal.
Customization
Structural styling via ::part(trigger), ::part(panel) and ::part(heading). Colours come from the overlay-surface semantic tokens (--c-surface-overlay, --c-on-surface); override the tokens to re-theme.
Examples
<template>
<div class="example-row">
<c-popover heading="Display settings">
<c-button slot="trigger" outlined>
Display settings
<c-icon :path="mdiTuneVariant" />
</c-button>
<div class="example-grid">
<c-switch v-model="compact">Compact rows</c-switch>
<c-switch v-model="showIds">Show identifiers</c-switch>
</div>
</c-popover>
<p>Compact: {{ compact }}, identifiers: {{ showIds }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { mdiTuneVariant } from '@mdi/js';
const compact = ref(false);
const showIds = ref(true);
</script>
API reference
<c-popover>
A click-opened, non-modal surface anchored to its trigger, floating in the top layer; its content may be interactive.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
distance | distance | number | string | 0 | Distance from the trigger to the panel, in pixels. Defaults to `0`. |
heading | heading | string | '' | Heading rendered at the top of the panel, doubling as its accessible name. Without it, set `aria-label` on the host. Named `heading`, not `title`: a `title` attribute on the host would trigger the browser's native tooltip and collides with `HTMLElement.title`. |
open | open | boolean | false | Whether the popover is open. Two-way: emits `change:open`. |
position | position | CPlacement | 'bottom' | Preferred placement of the panel relative to the trigger. |
trigger | trigger | HTMLElement | string | — | Designated trigger: an element elsewhere in the document that opens the popover — its document ID, or the element itself. The same trigger concept as the `trigger` slot, supplied by reference: the popover wires click-to-toggle, ARIA and focus return onto it and anchors the panel to it. When both routes are supplied, this prop wins over the slot. |
Events
| Event | Detail | Description |
|---|---|---|
change:open | boolean | Fired whenever the popover opens or closes, carrying the new open state. Named `change:open`, not `update:open`: Vue's runtime silently drops `onUpdate:*` listeners on custom elements (`isModelListener`), so a template `@update:open` would never be attached. |
Slots
| Slot | Description |
|---|---|
trigger | The element that opens the popover (e.g. a c-button) |
default | The popover's content; may contain interactive elements |
CSS parts
Style from outside with c-popover::part(name) — parts are the library's only styling customization API.
| Part | Description |
|---|---|
trigger | The inline wrapper around the slotted trigger, serving as the panel's anchor |
panel | The floating popover surface positioned against the trigger |
heading | The heading rendered from the `heading` prop |
Types
Importable from the package root: import type { … } from '@cscfi/csc-ui'
CPopoverProps
export interface CPopoverProps {
/** Distance from the trigger to the panel, in pixels. Defaults to `0`. */
distance?: number | string;
/**
* Heading rendered at the top of the panel, doubling as its accessible
* name. Without it, set `aria-label` on the host. Named `heading`, not
* `title`: a `title` attribute on the host would trigger the browser's
* native tooltip and collides with `HTMLElement.title`.
* @freeform
*/
heading?: string;
/** Whether the popover is open. Two-way: emits `change:open`. */
open?: boolean;
/** Preferred placement of the panel relative to the trigger. */
position?: CPlacement;
/**
* Designated trigger: an element elsewhere in the document that opens the
* popover — its document ID, or the element itself. The same trigger
* concept as the `trigger` slot, supplied by reference: the popover wires
* click-to-toggle, ARIA and focus return onto it and anchors the panel to
* it. When both routes are supplied, this prop wins over the slot.
*/
trigger?: HTMLElement | string;
}CPlacement shared
Preferred placement of a floating panel relative to its anchor, shared by the anchor-positioned overlay components (`c-menu`, `c-tooltip`, `c-popover`). The side names the panel's position; `-start`/`-end` align the panel's edge with the anchor's on the cross axis.
export type CPlacement =
| 'bottom-end'
| 'bottom-start'
| 'bottom'
| 'left-end'
| 'left-start'
| 'left'
| 'right-end'
| 'right-start'
| 'right'
| 'top-end'
| 'top-start'
| 'top';