Skip to content
v2.5.0
English
Русский
Documentation

Popover

Popover is a compound component for non-modal floating content anchored to a trigger.
popover.tsx · 18 lines
import {
  Button,
  Heading,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';

<Popover role='dialog'>
  <PopoverTrigger asChild><Button variant='secondary' appearance='ghost'>Storage · 82% used</Button></PopoverTrigger>
  <PopoverContent aria-labelledby='preview-popover-title'>
Browser support
Newly available 2025
  • Chrome 116
  • Edge 116
  • Firefox 125
  • Safari 17
  • Safari on iOS 18.3
Bundle size
Tested in
30 tests

Summary

Popover is a compound component for non-modal floating content anchored to a trigger. PopoverContent is a native popover="auto" element opened from the trigger through popovertarget, so the browser shows it in the top layer, closes it on an outside click or Escape, and it is never clipped by overflow or covered by a modal dialog. It sits between tooltip-level hinting and full modal overlays.

Imports

import {
  Popover,
  PopoverTrigger,
  PopoverContent,
  type PopoverProps,
} from '@vega-ui/react';

Exported Types

  • PopoverProps
  • PopoverTriggerProps
  • PopoverContentProps

Minimal Composition

minimal-composition.tsx · 27 lines
import { useId } from 'react';
import {
  Button,
  Heading,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';

export default function MinimalComposition() {
  const titleId = useId();

Required Parts

  • Popover: root state and context provider
  • PopoverTrigger: trigger surface
  • PopoverContent: floating content

Optional Parts

None. For a dimmed backdrop, use Dialog or Sheet instead.

Composition Order

  1. Popover
  2. PopoverTrigger
  3. PopoverContent

Variants

  • Trigger-driven or controlled open state
  • Lightweight informational popover versus richer custom content

Edge Cases

  • Use Dialog or Sheet instead when interaction should be modal or needs a dimmed backdrop.
  • Floating content must be tested inside scroll containers and overlays.
  • Trigger content should clearly indicate whether the popover is open.
  • If the content starts accumulating form sections, confirmations, or large action groups, the correct abstraction is usually no longer a popover.

Common Mistakes

  • Putting critical task flow inside a popover.
  • Forgetting to test placement inside clipped or scrolling containers.
  • Letting popover content grow into a mini-dialog with too much text or too many actions.

Examples

Basic

Basic: account menu

role='dialog' on the root gives the content role="dialog" and the trigger aria-haspopup="dialog", and aria-label names the content. Layout styles go on an inner element: a display set on PopoverContent itself would override the browser's hiding of a closed popover.

anchored-user-menu.tsx · 35 lines
import {
  Button,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Separator,
  Text,
} from '@vega-ui/react';

export default function AnchoredUserMenu() {
  return (
    <Popover role='dialog' placement='bottom-end'>

Controlled/Stateful

Controlled/Stateful: notifications that mark themselves read

onChangeOpen reports every open and close, from the trigger, an outside click or Escape. Opening clears the unread count, and the trigger's aria-label carries the count for screen readers.

controlled-open-state.tsx · 67 lines
import { useId, useState } from 'react';
import {
  Badge,
  Heading,
  Icon,
  IconButton,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';
import { BellIcon } from '@vega-ui/icons';

Controlled/Stateful: menu that closes on select

A popover does not close by itself when a button inside is pressed. Control open and close it from the item handler, which also records the chosen action.

menu-closes-on-select.tsx · 46 lines
import { useState } from 'react';
import {
  Button,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';

const actions = ['Duplicate', 'Move to folder', 'Download as PDF', 'Archive'];

export default function MenuClosesOnSelect() {

Controlled/Stateful: lightweight delete confirmation

Each comment has its own controlled popover with Keep and Delete. autoFocus puts focus on the safe answer when the popover opens, and the content is named by its question through aria-labelledby.

destructive-confirm-in-a-popover.tsx · 83 lines
import { useId, useState, type CSSProperties } from 'react';
import {
  Button,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';

const destructive = {
  '--button-background-color': 'var(--color-red-accent-500)',
  '--button-background-color-hover': 'var(--color-red-accent-600)',

Form/Integration

Form/Integration: inline rename form

The form validates natively (required, pattern, maxLength), reads the new name with FormData and closes the popover by setting open to false. The round close button is a plain IconButton that sets open to false, since there is no close part for popovers.

inline-rename-flow.tsx · 58 lines
import { useId, useState, type CSSProperties, type FormEvent } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Label,
  Popover,
  PopoverContent,
  PopoverTrigger,
  TextField,
  TextFieldInput,

Layout/Overlay

Layout/Overlay: popovers inside a scrolling list

Each row has its own popover. The content renders in the top layer, so the list's overflow: auto never clips it, and it follows its trigger while the list scrolls and flips when there is no room below.

scroll-container.tsx · 60 lines
import {
  Icon,
  IconButton,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';
import { InfoIcon } from '@vega-ui/icons';

const names = ['Maria Chen', 'Tom Okafor', 'Lea Novak', 'Sam Rivera', 'Ana Silva'];

Error

The form uses noValidate and checks the address on submit: an invalid one keeps the popover open and shows an error linked with aria-describedby and aria-invalid, a valid one is inserted and closes it. Submit the prefilled value to see the error.

link-error.tsx · 97 lines
import { useId, useState, type FormEvent } from 'react';
import {
  Button,
  HelperText,
  Icon,
  IconButton,
  Label,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
  TextField,

Disabled

Disabled: trigger disabled until something is selected

The Bulk actions trigger is disabled while no file is selected, and the hint is linked with aria-describedby while it is. A disabled button cannot open its popover through popovertarget.

disabled-trigger.tsx · 86 lines
import { useId, useState } from 'react';
import {
  Button,
  Checkbox,
  CheckboxCheckedIcon,
  CheckboxIndeterminateIcon,
  CheckboxIndicator,
  CheckboxInput,
  HelperText,
  Popover,
  PopoverContent,
  PopoverTrigger,

Edge

Edge: popover inside a popover

The label picker opens from inside the filters popover. Nested popover="auto" elements stack natively: choosing a label closes only the inner one, and an outside click closes both.

nested-popover.tsx · 55 lines
import { useState } from 'react';
import {
  Button,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';

const labels = ['Bug', 'Feature', 'Design', 'Docs'];

export default function NestedPopover() {

Edge: long unbreakable content

PopoverContent is width: max-content, so a long URL would make it as wide as the URL. A max-width on the inner element and overflow-wrap: anywhere keep it within the screen.

long-content.tsx · 25 lines
import {
  Button,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';

const webhook = 'https://hooks.northwind.dev/integrations/v2/workspaces/8f3a9c1e-42b7-4d0a-9e61-b5c7d2f40a18/events?signature=sha256';

export default function LongContent() {
  return (

Anatomy

Overview

Popover owns open state and floating-layer coordination between the trigger and content. It stays non-modal by default, so the composition contract is about anchoring and contextual content rather than isolation.

Required Parts

Popover

Required. Owns open state and anchor coordination.

PopoverTrigger

Required for trigger-driven usage. Toggles the popover through the native popovertarget, so it must be a button; with asChild, the child must render a <button>.

PopoverContent

Required. Renders the floating content surface as a native popover="auto" element in the top layer, so no portal part is needed. It is always rendered and hidden while closed; its children render only while it is open.

Optional Parts

None. For a dimmed backdrop, use Dialog or Sheet instead.

Composition Order

  1. Popover
  2. PopoverTrigger
  3. PopoverContent

Valid Composition Patterns

minimal-composition.tsx · 27 lines
import { useId } from 'react';
import {
  Button,
  Heading,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
} from '@vega-ui/react';

export default function MinimalComposition() {
  const titleId = useId();

Invalid Composition Patterns

Critical modal task inside PopoverContent

If the flow blocks progress or requires strong focus trapping, use Dialog, Drawer, or Sheet instead.

Large page-like layout inside the popover

This weakens the contextual nature of the component and usually indicates the wrong overlay pattern.

Trigger and content split across unrelated state owners

That makes open state and placement harder to reason about than a single popover tree.