When to Use
When the target element has further description and related operations, it can be included in the card and displayed according to the user's operational behavior.
The difference with Tooltip is that users can operate on elements on the floating layer, so it can carry more complex content, such as links or buttons.
Demos
How to import
import { Popover } from '@fibrous/semi-ui-react';
Cautions
Tooltip needs to apply DOM event listeners to children. If the child element is a custom component, you need to ensure that it can pass properties to the underlying DOM element
At the same time, in order to calculate the positioning of the popup layer, it is necessary to obtain the real DOM elements of the children, so Popover supports the following types of children
- Class Component, it is not mandatory to bind ref, but you need to ensure that props can be transparently transmitted to the real DOM node
- Use a functional component that passes props and
ref(a plain prop in React 19) to the real DOM node in children - Real DOM nodes, such as span, div, p...
Code
import React from 'react';
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-react';
import { Popover, Space, Empty } from '@fibrous/semi-ui-react';
const style = { border: '2px solid var(--semi-color-border)', paddingLeft: 4, paddingRight: 4, borderRadius: 4 };
// Pass props to the underlying DOM and bind the ref (a plain prop in React 19)
function FCChildren({ ref, ...props }: React.ComponentProps<'span'>) {
return (
<span {...props} ref={ref} style={style}>
Functional Component
</span>
);
}
class MyComponent extends React.Component {
render() {
return (
<span {...this.props} style={style}>
ClassComponent
</span>
);
}
}
const content = (
<Empty
title="先进的设计 / 研发协作方式"
image={<IllustrationSuccess style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationSuccessDark style={{ width: 150, height: 150 }} />}
description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
style={{ width: 400, margin: '0 auto', display: 'flex', padding: 20 }}
/>
);
export default function PopoverDemo() {
return (
<Space>
<Popover content={content}>
<FCChildren />
</Popover>
<Popover content={content}>
<MyComponent />
</Popover>
<Popover content={content}>
<span style={style}>DOM</span>
</Popover>
</Space>
);
}Basic Usage
Wrap the trigger with Popover as children (the trigger is a Tag in the example below) and pass the popup content through content.
The cautions are the same as Tooltip
Code
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-react';
import { Popover, Tag, Empty } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
return (
<Popover
content={
<Empty
title="先进的设计 / 研发协作方式"
image={<IllustrationSuccess style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationSuccessDark style={{ width: 150, height: 150 }} />}
description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
style={{ width: 400, margin: '0 auto', display: 'flex', padding: 20 }}
/>
}
>
<Tag>悬停此处</Tag>
</Popover>
);
}Pop-up Position
Support twelve directions.
Code
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-react';
import { Popover, Tag, Empty, type TooltipPosition as Position } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
const tops: [Position, string][] = [
['topLeft', 'TL'],
['top', 'Top'],
['topRight', 'TR'],
];
const lefts: [Position, string][] = [
['leftTop', 'LT'],
['left', 'Left'],
['leftBottom', 'LB'],
];
const rights: [Position, string][] = [
['rightTop', 'RT'],
['right', 'Right'],
['rightBottom', 'RB'],
];
const bottoms: [Position, string][] = [
['bottomLeft', 'BL'],
['bottom', 'Bottom'],
['bottomRight', 'BR'],
];
const article = (
<Empty
title="先进的设计 / 研发协作方式"
image={<IllustrationSuccess style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationSuccessDark style={{ width: 150, height: 150 }} />}
description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
style={{ width: 400, margin: '0 auto', display: 'flex', padding: 20 }}
/>
);
return (
<div>
<div style={{ marginLeft: 80, whiteSpace: 'nowrap' }}>
{tops.map(([position, label]) => (
<Popover content={article} position={position} key={position}>
<Tag style={{ margin: 8, padding: 20 }}>{label}</Tag>
</Popover>
))}
</div>
<div style={{ width: 80, float: 'left' }}>
{lefts.map(([position, label]) => (
<Popover content={article} position={position} key={position}>
<Tag style={{ margin: 8, padding: 20, width: 60 }}>{label}</Tag>
</Popover>
))}
</div>
<div style={{ width: 40, marginLeft: 300 }}>
{rights.map(([position, label]) => (
<Popover content={article} position={position} key={position}>
<Tag style={{ margin: 8, padding: 20, width: 60 }}>{label}</Tag>
</Popover>
))}
</div>
<div style={{ marginLeft: 80, clear: 'both', whiteSpace: 'nowrap' }}>
{bottoms.map(([position, label]) => (
<Popover content={article} position={position} key={position}>
<Tag style={{ margin: 8, padding: 20, width: 60 }}>{label}</Tag>
</Popover>
))}
</div>
</div>
);
}Controlled Display
In this scenario, Popover's display is completely at the control of parameter visible.
Code
import { useState } from 'react';
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-react';
import { Popover, RadioGroup, Radio, Empty } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
const content = (
<Empty
title="先进的设计 / 研发协作方式"
image={<IllustrationSuccess style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationSuccessDark style={{ width: 150, height: 150 }} />}
description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
style={{ width: 400, margin: '0 auto', display: 'flex', padding: 20 }}
/>
);
const [visible, setVisible] = useState(false);
return (
<Popover visible={visible} content={content} trigger="custom">
<RadioGroup type="button" onChange={(e) => setVisible(e.target.value)} value={visible}>
<Radio value>受控显示</Radio>
<Radio value={false}>受控隐藏</Radio>
</RadioGroup>
</Popover>
);
}Conditional trigger (condition)
When condition={false}, Popover will not respond to hover/click/focus triggers (does not affect trigger='custom'); when condition changes from true to false, an open popup closes at once; with visible controlled it only calls onVisibleChange(false), and the popup closes once the owner sets visible to false.
Code
import { useMemo, useState } from 'react';
import { Popover, Button, Switch, Space, Typography } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
const [enabled, setEnabled] = useState(true);
const content = useMemo(() => `condition is ${enabled ? 'true' : 'false'}`, [enabled]);
return (
<Space align="center">
<Typography.Text id="popover-condition-label">condition</Typography.Text>
<Switch aria-labelledby="popover-condition-label" checked={enabled} onChange={(v) => setEnabled(v)} />
<Popover content={content} condition={enabled}>
<Button>Hover me</Button>
</Popover>
<Popover content={content} trigger="click" condition={enabled}>
<Button>Click me</Button>
</Popover>
</Space>
);
}Show Small Triangle
Popover also supports the display of a small triangle.
The floating layer in this mode has a default style that you can overwrite by passing the style parameters.
Code
import { Popover, Tag, type TooltipPosition as Position } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
const tops: [Position, string][] = [
['topLeft', 'TL'],
['top', 'Top'],
['topRight', 'TR'],
];
const lefts: [Position, string][] = [
['leftTop', 'LT'],
['left', 'Left'],
['leftBottom', 'LB'],
];
const rights: [Position, string][] = [
['rightTop', 'RT'],
['right', 'Right'],
['rightBottom', 'RB'],
];
const bottoms: [Position, string][] = [
['bottomLeft', 'BL'],
['bottom', 'Bottom'],
['bottomRight', 'BR'],
];
return (
<div style={{ paddingLeft: 40 }} className="tag-margin-right">
<div style={{ marginLeft: 40, whiteSpace: 'nowrap' }}>
{tops.map(([position, label]) => (
<Popover
showArrow
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
<div style={{ width: 40, float: 'left' }}>
{lefts.map(([position, label]) => (
<Popover
showArrow
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
<div style={{ width: 40, marginLeft: 180 }}>
{rights.map(([position, label]) => (
<Popover
showArrow
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
<div style={{ marginLeft: 40, clear: 'both', whiteSpace: 'nowrap' }}>
{bottoms.map(([position, label]) => (
<Popover
showArrow
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
</div>
);
}Arrow Point at Center
Under the condition of showArrow=true, you can pass in arrowPointAtCenter=true so that the small triangle always points to the center of the element.
Code
import { Popover, Tag, type TooltipPosition as Position } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
const tops: [Position, string][] = [
['topLeft', 'TL'],
['top', 'Top'],
['topRight', 'TR'],
];
const lefts: [Position, string][] = [
['leftTop', 'LT'],
['left', 'Left'],
['leftBottom', 'LB'],
];
const rights: [Position, string][] = [
['rightTop', 'RT'],
['right', 'Right'],
['rightBottom', 'RB'],
];
const bottoms: [Position, string][] = [
['bottomLeft', 'BL'],
['bottom', 'Bottom'],
['bottomRight', 'BR'],
];
return (
<div style={{ paddingLeft: 40 }} className="tag-margin-right">
<div style={{ marginLeft: 40, whiteSpace: 'nowrap' }}>
{tops.map(([position, label]) => (
<Popover
showArrow
arrowPointAtCenter
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
<div style={{ width: 40, float: 'left' }}>
{lefts.map(([position, label]) => (
<Popover
showArrow
arrowPointAtCenter
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
<div style={{ width: 40, marginLeft: 180 }}>
{rights.map(([position, label]) => (
<Popover
showArrow
arrowPointAtCenter
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
<div style={{ marginLeft: 40, clear: 'both', whiteSpace: 'nowrap' }}>
{bottoms.map(([position, label]) => (
<Popover
showArrow
arrowPointAtCenter
content={
<article>
Hi ByteDancer, this is a popover.
<br /> We have 2 lines.
</article>
}
position={position}
key={position}
>
<Tag>{label}</Tag>
</Popover>
))}
</div>
</div>
);
}Set Floating Layer Background Color
If you need to customize the background color or border color of the floating layer, please Be sure to set backgroundColor and borderColor properties in style separately. This enables the "small triangle" to apply the same background color and border color.
Code
import { Popover, Tag } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
return (
<div>
<Popover
aria-label="Colorful Popover"
content={<article style={{ padding: 4 }}>Hi, Semi UI Popover.</article>}
trigger="custom"
visible
position="right"
showArrow
style={{
backgroundColor: 'var(--semi-blue-6)',
borderColor: 'var(--semi-blue-6)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<Tag
style={{
backgroundColor: 'var(--semi-blue-6)',
color: 'var(--semi-color-white)',
}}
>
Colorful Popover
</Tag>
</Popover>
</div>
);
}Initialize the Focus Position of Popup Layer
Popover content also supports functions. Its input parameter is an object, which binds initialFocusRef to the focusable DOM or component. When the panel is opened, it will automatically focus at that position.
Code
import { Button, Input, Popover, Space, type InputHandle, type TooltipRenderContentProps } from '@fibrous/semi-ui-react';
export default function PopoverDemo() {
const renderContent = ({ initialFocusRef }: TooltipRenderContentProps<InputHandle>) => {
return (
<div style={{ padding: 12 }}>
<Space>
<Button>first focusable element</Button>
<Input ref={initialFocusRef} placeholder="focus here" />
</Space>
</div>
);
};
return (
<Popover content={renderContent} trigger="click">
<Button>click me</Button>
</Popover>
);
}Use with Tooltip or Popconfirm
Please refer to Use with Tooltip/Popconfirm
API Reference
Props
| Property | Description | Type | Default |
|---|---|---|---|
| afterClose | Called after the popup has closed and its leave transition has ended. | Type() => void | - |
| afterOpen | Called after the popup has opened and its enter transition has ended, or right after opening when there is no enter transition. An opening that is closed before the transition ends does not call it. | TooltipProps['afterOpen'] | - |
| aria-label | Name of the popup when its role is dialog. | string | - |
| aria-labelledby | id of the element that names the popup when its role is dialog. | string | - |
| arrowPointAtCenter | Whether the arrow points at the center of the trigger. Requires showArrow. Unset, it is true. | boolean | - |
| arrowStyle | borderColor, backgroundColor and borderOpacity of the arrow. Colors that are not set follow backgroundColor and borderColor in style. | ArrowStyle | - |
| autoAdjustOverflow | Whether the popup flips automatically when it would be clipped. | boolean | true |
| children | The trigger. Event listeners are applied to it and its real DOM node sets the position, so a component must pass its props and ref down to its root DOM node. | React.ReactNode | - |
| className | Class name of the popup. | string | - |
| clickToHide | Whether a click on the popup or anything inside it closes the popup. | TooltipProps['clickToHide'] | - |
| closeOnEsc | Whether Esc closes the open popup. With trigger custom it only calls onEscKeyDown; with a controlled visible it only calls onVisibleChange(false) and the popup closes once the caller writes back. | TooltipProps['closeOnEsc'] | true |
| condition | Whether the popup may be triggered. Unset, it is true. Only an explicit false disables the hover, click and focus triggers; with trigger custom, visible still shows the popup. Changing it from true to false closes an open popup at once; with a controlled visible it only calls onVisibleChange(false) and the popup closes once the caller writes back. | boolean | - |
| content | Content of the popup. Pass a ReactNode or a function; the function receives { initialFocusRef }, and binding initialFocusRef as the ref of a focusable element focuses it when the popup opens. | TooltipProps['content'] | - |
| contentClassName | Class name of the content card in the popup. | string | any[] | - |
| disableArrowKeyDown | Whether the arrow keys on the trigger are prevented from moving focus into the popup. | TooltipProps['disableArrowKeyDown'] | - |
| disableFocusListener | Whether keyboard focus is ignored when trigger is hover. | boolean | - |
| guardFocus | Whether Tab cycles inside the popup while focus is in it. | TooltipProps['guardFocus'] | true |
| keepDOM | Whether the popup content stays mounted while closed. | boolean | - |
| motion | Whether the popup is animated. | boolean | true |
| mouseEnterDelay | Milliseconds before the popup shows after the pointer enters. Applies to the hover and focus triggers. Unset, it is 50. | number | - |
| mouseLeaveDelay | Milliseconds before the popup hides after the pointer leaves. Applies to the hover and focus triggers; not less than mouseEnterDelay. Unset, it is 50. | number | - |
| onClickOutSide | Called with the native MouseEvent when the popup is open and the click lands outside both the trigger and the popup. Applies to the custom and click triggers only. | TooltipProps['onClickOutSide'] | - |
| onEscKeyDown | Called with the native KeyboardEvent when Esc is pressed while the popup is open. | TooltipProps['onEscKeyDown'] | - |
| onOpen | Called once per opening when the popup is shown and its enter transition starts, before afterOpen. Not called for a hover opening that is withdrawn because the pointer already left. | TooltipProps['onOpen'] | - |
| onVisibleChange | Called when the popup shows or hides. With a controlled visible it only reports requests from user interaction; changing visible yourself does not call it. | Type(visible: boolean) => void | - |
| position | Position of the popup relative to the trigger: top, topLeft, topRight, left, leftTop, leftBottom, right, rightTop, rightBottom, bottom, bottomLeft or bottomRight. | Position | "bottom" |
| prefixCls | Class prefix of the popup. A custom value drops the Popover styles. | string | "semi-popover" |
| ref | Receives the Popover handle with the methods listed under Methods. | TypeReact.Ref<PopoverHandle> | - |
| returnFocusOnClose | Whether focus returns to the trigger after Esc closes the popup. | TooltipProps['returnFocusOnClose'] | true |
| role | ARIA role of the popup. Omitted, it is dialog for the click, contextMenu and custom triggers and tooltip for the rest; pass presentation when the popup only belongs to another control, such as a combobox list. | string | - |
| showArrow | Whether the arrow is shown. | boolean | false |
| spacing | Distance in pixels between the popup and the trigger. An object sets the horizontal and vertical distances. Omitted, it is 4 without an arrow and 10 with one. | number | { x: number; y: number } | - |
| stopPropagation | Whether click events on the popup stop propagating. | boolean | string | - |
| style | Inline style of the popup. With the arrow shown, the arrow reads backgroundColor and borderColor from it. | CSSProperties | - |
| trigger | When the popup shows: hover, focus, click, custom or contextMenu. With custom, visible alone decides. | Trigger | "hover" |
| visible | Whether the popup is shown (controlled). Once given, the trigger, outside clicks and Esc only request a change through onVisibleChange and take effect after the caller writes back. Use it with trigger custom when the trigger events are not wanted. | boolean | - |
Methods
| Method | Description | Type |
|---|---|---|
| focusTrigger | Focuses the trigger. Takes effect only when returnFocusOnClose is true and trigger is not custom. | Type() => void |
| rePosition | Recomputes the popup position from the trigger's current position at once. The popup already follows scrolling, window resizing, trigger moves and size changes of the trigger or popup, so this is rarely needed; following a trigger move lands a frame late, so call this to align within the same frame. | Type() => void |
Other undeclared attributes are dropped (Popover has no root DOM element); className and style land on the popup, and the trigger attributes are only added to the trigger element.
Methods are bound to the Popover instance and can be called through ref for special interactions.
Accessibility
ARIA
- About role
- If the trigger is set to
click,contextMenuorcustom, the PopoverContent element has role set todialog. - If the trigger is set to
hoverorfocus, it has role set totooltip. - The
roleprop overrides the default. Adialogneeds a name: passaria-label, or pointaria-labelledbyat a heading in the content; an unnamed dialog logs a warning in development
- If the trigger is set to
- Popover's content
- The content wrapper will be automatically added with the id attribute
- Popover's children (for a
tooltipthey only getaria-describedby; for other roles, such aspresentation, nothing is added)- Will be automatically added aria-expanded attribute, when Popover is visible, the attribute value is
true, when invisible Isfalse - Will be automatically added aria-haspopup attribute, which is
dialog - Will be automatically added aria-controls attribute, which is the id of the content wrapper
- Will be automatically added aria-expanded attribute, when Popover is visible, the attribute value is
Keyboard and Focus
- When the Popover trigger method is set to
hover: Open the Popover when the mouse is hovered or focused - When the Popover trigger method is set to
click: Click the trigger or focus and use the Enter key to open the Popover - After the Popover is activated, press the
arrow key⬇️ to move the focus to the Popover. At this time, the focus is on the first interactive element in the Popover by default, and the user can also customize the focus position (if there is no interactive element in the Popover, it will appear as No response) - Use the
Tabkey when the focus is in the Popover, the focus will cycle in the Popover, and useShift + Tabto move the focus in the opposite direction - Keyboard users can close the Popover by pressing
Esc, after closing the focus returns to the trigger (when the trigger is click)
Design Tokens
| Sass variable | Value | Usage |
|---|---|---|
| $animation_duration-popover-out | 100ms | popover-显示及收起动画-动画持续时间 |
| $animation_function-popover-out | cubic-bezier(0.215, 0.61, 0.355, 1) | popover-显示及收起动画-动画插值函数 |
| $horizontal-rate-popover | 0.24 | 水平方向矫正因子 |
| $color-popover_arrow-icon-default | unset | 箭头图标 |
| $color-popover-bg-default | var(--semi-color-bg-3) | 默认背景色 |
| $color-popover-border-default | var(--semi-color-border) | 默认描边颜色 |
| $color-popover-arrow-border | var(--semi-color-border) | 箭头描边颜色 |
| $color-popover-arrow-bg | var(--semi-color-bg-3) | 箭头默认背景色 |
| $width-popover_arrow | 24px | 水平箭头宽度 |
| $height-popover_arrow | 8px | 水平箭头高度 |
| $height-popover_arrow_vertical | 24px | 垂直箭头高度 |
| $width-popover_arrow_vertical | 8px | 垂直箭头宽度 |
| $width-popover_title-border | 1px | |
| $spacing-popover_title-padding | 8px | 气泡卡片标题内边距 |
| $spacing-popover_withArrow-padding | 12px | 带箭头的气泡卡片标题内边距 |
| $spacing-popover_arrow_adjusted_offset-y | 6px | 垂直方向上的校正偏移量,主要修正三角形有棱角的问题 |
| $spacing-popover_arrow_adjusted_offset-x | math.round($horizontal-rate-popover * $width-popover_arrow) | 水平方向上的校正偏移量,主要修正三角形有棱角的问题 |
| $radius-popover | var(--semi-border-radius-medium) | 气泡卡片的圆角大小 |
| $filter-popover-bg | none |
FAQ
-
Why the position of the popover overlay card and the relative position of the overlay trigger are not as expected?
Popover relies on Tooltip at the bottom. In order to calculate positioning, Tooltip needs to obtain the real DOM elements of children. Therefore, the Popover type currently supports the following types of children:- The Jsx type of the real dom node, such as span, div, p...
- Use a FunctionComponent that passes props and
ref(a plain prop in React 19) to the real dom node
When Semi Input with prefix is used as children, even if the same width of Input and Popover content are set, the position of the floating card is still positioned relative to the input box that does not contain the prefix part. At this time, just put another div on the outer layer of the children to solve the problem.
-
Why does the popover layer card lose its width and wrap unexpectedly when the width is not enough near the screen border?
After Chromium 104, the wrapping rendering strategy when the width of the screen border text is not enough has changed. For details, see issue #1022, the semi-side has been This problem was fixed in v2.17.0.