CSC Design System next
VueFlavour Vue React Angular TypeScript PrimaryCSC UI ColorsPrimarySecondaryAccentCustom ColorsRedOrangeGreenBluePurplePink
Guides Getting started Customization Data visualization Migration guide Componentsc-accordionc-alertc-autocompletec-badgec-buttonc-button-groupc-cardc-checkboxc-csc-logoc-data-tablec-dividerc-iconc-icon-buttonc-inputc-linkc-listc-loaderc-login-buttonsc-login-cardc-mainc-menuc-messagec-modalc-navigation-buttonc-otp-inputc-pagec-paginationc-popoverc-progress-barc-progress-circlec-radio-groupc-selectc-side-navigationc-sliderc-spinnerc-statusc-stepsc-switchc-tablec-tabsc-tagsc-text-fieldc-toastsc-toolbarc-tooltip

<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

Basic
Vue React Angular TypeScript
<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

PropertyAttributeTypeDefaultDescription
distancedistancenumber | string0Distance from the trigger to the panel, in pixels. Defaults to `0`.
headingheadingstring''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`.
openopenbooleanfalseWhether the popover is open. Two-way: emits `change:open`.
positionpositionCPlacement'bottom'Preferred placement of the panel relative to the trigger.
triggertriggerHTMLElement | stringDesignated 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

EventDetailDescription
change:openbooleanFired 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

SlotDescription
triggerThe element that opens the popover (e.g. a c-button)
defaultThe 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.

PartDescription
triggerThe inline wrapper around the slotted trigger, serving as the panel's anchor
panelThe floating popover surface positioned against the trigger
headingThe 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';