diff --git a/src/popover/README.md b/src/popover/README.md
new file mode 100644
index 000000000..5772a8d1b
--- /dev/null
+++ b/src/popover/README.md
@@ -0,0 +1,32 @@
+# Popover
+
+### Install
+
+```js
+import Vue from 'vue';
+import { Popover } from 'vant';
+
+Vue.use(Popover);
+```
+
+## Usage
+
+### Basic Usage
+
+```html
+
+```
+
+## API
+
+### Props
+
+| Attribute | Description | Type | Default |
+| --------- | ----------- | ---- | ------- |
+
+
+### Events
+
+| Event | Description | Arguments |
+| ----- | ----------- | --------- |
+
diff --git a/src/popover/README.zh-CN.md b/src/popover/README.zh-CN.md
new file mode 100644
index 000000000..e67674d25
--- /dev/null
+++ b/src/popover/README.zh-CN.md
@@ -0,0 +1,66 @@
+# Popover 气泡弹出框
+
+### 介绍
+
+### 引入
+
+```js
+import Vue from 'vue';
+import { Popover } from 'vant';
+
+Vue.use(Popover);
+```
+
+## 代码演示
+
+### 基础用法
+
+```html
+
+```
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| v-model | 是否展示气泡弹出层 | _boolean_ | `false` |
+| actions | 选项列表 | _Action[]_ | `[]` |
+| placement | 弹出位置 | _string_ | - |
+| theme | 主题风格,可选值为 `light` | _string_ | `dark` |
+| text-color | 自定义文字颜色 | _string_ | - |
+| background-color | 自定义背景颜色 | _string_ | - |
+| overlay | 是否显示遮罩层 | _boolean_ | `false` |
+| close-on-click-action | 是否在点击选项后关闭 | _boolean_ | `false` |
+| close-on-click-outside | 是否在点击外部元素后关闭菜单 | _boolean_ | `true` |
+| get-container `v2.4.4` | 指定挂载的节点,[用法示例](#/zh-CN/popup#zhi-ding-gua-zai-wei-zhi) | _string \| () => Element_ | - |
+
+### Action 数据结构
+
+`actions` 属性是一个由对象构成的数组,数组中的每个对象配置一列,对象可以包含以下值:
+
+| 键名 | 说明 | 类型 |
+| --------- | ------------------------ | -------- |
+| text | 文字内容 | _string_ |
+| className | 为对应选项添加额外的类名 | _any_ |
+
+### placement 可选值
+
+top left right bottom topLeft topRight bottomLeft bottomRight leftTop leftBottom rightTop rightBottom
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+| ------ | ------------------------ | ------------------------------- |
+| select | 点击选项时触发 | _action: Action, index: number_ |
+| open | 打开菜单时触发 | - |
+| close | 关闭菜单时触发 | - |
+| opened | 打开菜单且动画结束后触发 | - |
+| closed | 关闭菜单且动画结束后触发 | - |
+
+### Slots
+
+| 名称 | 说明 |
+| ------- | -------------------- |
+| default | 自定义菜单的展示内容 |
diff --git a/src/popover/demo/index.vue b/src/popover/demo/index.vue
new file mode 100644
index 000000000..98940a1db
--- /dev/null
+++ b/src/popover/demo/index.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+ {{ t('showPopover') }}
+
+
+
+
+
+
+
+ {{ t('lightTheme') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/popover/index.js b/src/popover/index.js
new file mode 100644
index 000000000..93b722a53
--- /dev/null
+++ b/src/popover/index.js
@@ -0,0 +1,76 @@
+import { createNamespace } from '../utils';
+
+// Mixins
+import { ClickOutsideMixin } from '../mixins/click-outside';
+
+// Components
+import Popup from '../popup';
+
+const [createComponent, bem] = createNamespace('popover');
+
+export default createComponent({
+ mixins: [
+ ClickOutsideMixin({
+ event: 'click',
+ method: 'onClickOutside',
+ }),
+ ],
+
+ props: {
+ value: Boolean,
+ overlay: Boolean,
+ placement: String,
+ textColor: String,
+ getContainer: [String, Function],
+ backgroundColor: String,
+ closeOnClickAction: Boolean,
+ theme: {
+ type: String,
+ default: 'dark',
+ },
+ actions: {
+ type: Array,
+ default: () => [],
+ },
+ },
+
+ methods: {
+ renderAction(action) {
+ return
{action.text}
;
+ },
+
+ onToggle(value) {
+ this.$emit('input', value);
+ },
+
+ onClickAction(action, index) {
+ this.$emit('select', action, index);
+
+ if (this.closeOnClickAction) {
+ this.$emit('input', false);
+ }
+ },
+
+ onClickOutside() {
+ this.$emit('input', false);
+ },
+ },
+
+ render() {
+ return (
+
+
+ {this.actions.map(this.renderAction)}
+
+ {this.slots('default')}
+
+ );
+ },
+});
diff --git a/src/popover/index.less b/src/popover/index.less
new file mode 100644
index 000000000..a51896139
--- /dev/null
+++ b/src/popover/index.less
@@ -0,0 +1,61 @@
+@import '../style/var';
+@import '../style/mixins/hairline';
+
+.van-popover {
+ position: absolute;
+ border-radius: @border-radius-lg;
+ transform: none;
+ transition: all 0.3s;
+
+ &__action {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 128px;
+ height: 44px;
+ font-size: @font-size-md;
+
+ &::after {
+ .hairline-bottom(@cell-border-color, @padding-md, @padding-md);
+ }
+
+ &:last-child::after {
+ display: none;
+ }
+ }
+
+ &--light {
+ color: @text-color;
+ background-color: @white;
+ box-shadow: 0 2px 12px rgba(50, 50, 51, 0.12);
+
+ .van-popover__action {
+ &:active {
+ background-color: @active-color;
+ }
+ }
+ }
+
+ &--dark {
+ color: @white;
+ background-color: @gray-8;
+ opacity: 0.9;
+
+ .van-popover__action {
+ &::after {
+ border-color: @gray-7;
+ }
+
+ &:active {
+ opacity: @active-opacity;
+ }
+ }
+ }
+
+ &-zoom-enter,
+ &-zoom-leave-active {
+ transform: scale(0.8);
+ opacity: 0;
+ }
+}
diff --git a/src/popover/test/__snapshots__/demo.spec.js.snap b/src/popover/test/__snapshots__/demo.spec.js.snap
new file mode 100644
index 000000000..7c04f9b40
--- /dev/null
+++ b/src/popover/test/__snapshots__/demo.spec.js.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders demo correctly 1`] = `
+
+
+
+
span: 8
+
span: 8
+
span: 8
+
+
+
span: 4
+
+ offset: 4, span: 10
+
+
+
+
+ offset: 12, span: 12
+
+
+
+
+
+
span: 8
+
span: 8
+
span: 8
+
+
+
+
+
span: 6
+
span: 6
+
span: 6
+
+
+
span: 6
+
span: 6
+
span: 6
+
+
+
span: 6
+
span: 6
+
span: 6
+
+
+
span: 6
+
span: 6
+
span: 6
+
+
+
span: 6
+
span: 6
+
span: 6
+
+
+
+`;
diff --git a/src/popover/test/__snapshots__/index.spec.js.snap b/src/popover/test/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..2d4588095
--- /dev/null
+++ b/src/popover/test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`gutter prop 1`] = `
+
+
24
+
12
+
12
+
8
+
8
+
8
+
6
+
6
+
6
+
6
+
7
+
6
+
5
+
4
+
3
+
2
+
+`;
diff --git a/src/popover/test/demo.spec.js b/src/popover/test/demo.spec.js
new file mode 100644
index 000000000..5c70922b5
--- /dev/null
+++ b/src/popover/test/demo.spec.js
@@ -0,0 +1,4 @@
+import Demo from '../demo';
+import { snapshotDemo } from '../../../test/demo';
+
+snapshotDemo(Demo);
diff --git a/src/popover/test/index.spec.js b/src/popover/test/index.spec.js
new file mode 100644
index 000000000..0ccd98fb1
--- /dev/null
+++ b/src/popover/test/index.spec.js
@@ -0,0 +1,2 @@
+// import Popover from '..';
+// import { mount } from '../../../test';
diff --git a/vant.config.js b/vant.config.js
index d025939c2..d7c9046dc 100644
--- a/vant.config.js
+++ b/vant.config.js
@@ -275,6 +275,10 @@ module.exports = {
path: 'notice-bar',
title: 'NoticeBar 通知栏',
},
+ {
+ path: 'popover',
+ title: 'Popover 气泡弹出框',
+ },
{
path: 'progress',
title: 'Progress 进度条',
@@ -638,6 +642,10 @@ module.exports = {
path: 'notice-bar',
title: 'NoticeBar',
},
+ {
+ path: 'popover',
+ title: 'Popover',
+ },
{
path: 'progress',
title: 'Progress',