- bottom
- bottom-start
- bottom-end
- top
- top-start
- top-end
- right
- right-start
- right-end
- left
- left-start
- left-end
Popover
A Popover is a non-disruptive container that is overlaid on a web page or app, positioned near its trigger, in order to present necessary information and tasks.
| Name | Value |
|---|---|
| Props | |
title | |
icon | |
useCloseButton | |
placement | Choose an option |
stackedActions | |
usePrimaryAction | |
primaryActionLabel | |
primaryActionType | |
useDefaultAction | |
defaultActionLabel | |
useBottomSheet | |
hideBackdrop | |
| Slots | |
default | |
| View | |
Reading direction | |
Note: For icon properties, the relevant icon also needs to be imported from the @wikimedia/codex-icons package. See the usage documentation for more information. | |
Overview
When to use Popover
The Popover component is intended to be used with other components such as a ToggleButton or Link. The Popover is displayed when the user interacts with the corresponding trigger element.
Use a ToggleButton as a trigger to emphasize what opened the Popover, setting the ToggleButton into its toggled state when triggering the Popover.
Use a Link as a trigger when the Popover is meant to serve as a preview for what pressing the link will present.
Popovers facilitate communication between the system and user. They perform best when used for additional information or as a workflow within a bigger task, as they don’t require loading a new page and keep actions in context.
Popovers differ from Dialogs as they are not as disruptive, since the context of the page around the Popover remains, as there is no visual overlay on the page. That being said, they should be used sparingly and only when necessary, since they inherently hide content and require an additional action to reveal it.
Use the Popover component when:
- Additional information needs to be displayed and separated from the page content.
- It is helpful to have the context of the page still within view.
Avoid using Popover when:
- The information can be displayed within the main interface.
- The content is long or complex, like a form with numerous fields.
- Typed input is required from the user.
- Components that take up a large amount of space need to be used in the content.
- The information being shown is a very brief message or explanation for an element on the page. Instead, use Tooltip.
About Popover
Popover includes the following elements.
Header
The Popover header can contain a decorative icon, title, or standard text. A quiet, icon-only Button may be used to close the Popover.
Body
Interactive types of content or components can be included within the Popover’s body—typically Fields or other form element types of components.
Avoid using Cards, other elevated components, or components requiring a lot of space within the Popover.
Footer
Action buttons should appear at the start of the Popover. A primary Button (either progressive or destructive) is used to indicate the main action. A normal neutral Button can be used to indicate a default action (e.g. “Cancel”). Additionally, icon-only Buttons can be used as needed.
Stack action buttons based on text length when needed, placing the primary button on top.
Don't stack action buttons when they can be placed side by side.
Arrow
Popovers have an arrow which points to the trigger.
Examples
Basic usage
This example includes text, a custom placement, and the ability to manually dismiss the Popover.
Use the header to align standard text content to the dismiss action.
<template>
<cdx-toggle-button
ref="triggerElement"
v-model="showPopover"
aria-label="Alert notification"
@update:model-value="onUpdate"
>
<cdx-icon :icon="cdxIconBell" />
</cdx-toggle-button>
<cdx-popover
v-model:open="showPopover"
:anchor="triggerElement"
placement="bottom-start"
:render-in-place="true"
title="Alerts"
:use-close-button="true"
:icon="cdxIconBell"
:primary-action="primaryAction"
:default-action="defaultAction"
@primary="showPopover = false"
@default="showPopover = false"
>
2 alerts from Wikitech and MediaWiki.
</cdx-popover>
</template>
<script>
import { defineComponent, ref } from 'vue';
import { CdxPopover, CdxToggleButton, CdxIcon } from '@wikimedia/codex';
import { cdxIconBell } from '@wikimedia/codex-icons';
export default defineComponent( {
name: 'PopoverBasic',
components: {
CdxPopover,
CdxToggleButton,
CdxIcon
},
setup() {
// Template ref for the Popover's trigger element needed to properly position the Popover.
const triggerElement = ref();
// Toggle the button's state and popover visibility.
const showPopover = ref( false );
// When the toggle button's state changes, an event is emitted to update
// the state in the parent component.
const onUpdate = function ( value ) {
// eslint-disable-next-line no-console
console.log( 'update:modelValue event emitted with value: ' + value );
};
// Footer action buttons.
const defaultAction = { label: 'Cancel' };
const primaryAction = { label: 'View notifications', actionType: 'progressive' };
return {
showPopover,
onUpdate,
triggerElement,
cdxIconBell,
defaultAction,
primaryAction
};
}
} );
</script><template>
<cdx-toggle-button
ref="triggerElement"
v-model="showPopover"
aria-label="Alert notification"
@update:model-value="onUpdate"
>
<cdx-icon :icon="cdxIconBell"></cdx-icon>
</cdx-toggle-button>
<cdx-popover
v-model:open="showPopover"
:anchor="triggerElement"
placement="bottom-start"
:render-in-place="true"
title="Alerts"
:use-close-button="true"
:icon="cdxIconBell"
:primary-action="primaryAction"
:default-action="defaultAction"
@primary="showPopover = false"
@default="showPopover = false"
>
2 alerts from Wikitech and MediaWiki.
</cdx-popover>
</template>
<script>
const { defineComponent, ref } = require( 'vue' );
const { CdxPopover, CdxToggleButton, CdxIcon } = require( '@wikimedia/codex' );
const { cdxIconBell } = require( './icons.json' );
module.exports = defineComponent( {
name: 'PopoverBasic',
components: {
CdxPopover,
CdxToggleButton,
CdxIcon
},
setup() {
// Template ref for the Popover's trigger element needed to properly position the Popover.
const triggerElement = ref();
// Toggle the button's state and popover visibility.
const showPopover = ref( false );
// When the toggle button's state changes, an event is emitted to update
// the state in the parent component.
const onUpdate = function ( value ) {
// eslint-disable-next-line no-console
console.log( 'update:modelValue event emitted with value: ' + value );
};
// Footer action buttons.
const defaultAction = { label: 'Cancel' };
const primaryAction = { label: 'View notifications', actionType: 'progressive' };
return {
showPopover,
onUpdate,
triggerElement,
cdxIconBell,
defaultAction,
primaryAction
};
}
} );
</script>Developer notes
Create a template ref for the trigger element, and then pass that ref to the anchor prop. The anchor prop is required to correctly position the Popover.
Ensure the toggle button's on/off state and the popover's visibility is synchronized via v-model.
Usage in TypeScript
Vue 3.5 introduced the useTemplateRef() composable to simplify the creation of template refs in Vue components using the Composition API. If you are using TypeScript, consider annotating the types for any template refs like this:
// Basic component typing with ComponentPublicInstance.
const anchorRef = useTemplateRef<ComponentPublicInstance>("my-anchor-id");More information on typing component template refs can be found in the Vue.js docs.
Stacked actions
The action buttons in the footer are stacked vertically on narrow screens, but appear side by side on wide screens. In some situations, like when the button text is long, you can force the action buttons to always be stacked vertically regardless of screen size.
<template>
<cdx-toggle-button
ref="triggerElement"
v-model="showPopover"
aria-label="Alert notification"
@update:model-value="onUpdate"
>
<cdx-icon :icon="cdxIconBell" />
</cdx-toggle-button>
<cdx-popover
v-model:open="showPopover"
:anchor="triggerElement"
placement="bottom-start"
:render-in-place="true"
title="Alerts"
:use-close-button="true"
:icon="cdxIconBell"
:primary-action="primaryAction"
:default-action="defaultAction"
:stacked-actions="true"
@primary="showPopover = false"
@default="showPopover = false"
>
2 alerts from Wikitech and MediaWiki.
</cdx-popover>
</template>
<script>
import { defineComponent, ref } from 'vue';
import { CdxPopover, CdxToggleButton, CdxIcon } from '@wikimedia/codex';
import { cdxIconBell } from '@wikimedia/codex-icons';
export default defineComponent( {
name: 'PopoverStackedActions',
components: {
CdxPopover,
CdxToggleButton,
CdxIcon
},
setup() {
// Template ref for the Popover's trigger element needed to properly position the Popover.
const triggerElement = ref();
// Toggle the button's state and popover visibility.
const showPopover = ref( false );
// When the toggle button's state changes, an event is emitted to update
// the state in the parent component.
const onUpdate = function ( value ) {
// eslint-disable-next-line no-console
console.log( 'update:modelValue event emitted with value: ' + value );
};
// Footer action buttons.
const defaultAction = { label: 'Cancel' };
const primaryAction = { label: 'View notifications', actionType: 'progressive' };
return {
showPopover,
onUpdate,
triggerElement,
cdxIconBell,
defaultAction,
primaryAction
};
}
} );
</script><template>
<cdx-toggle-button
ref="triggerElement"
v-model="showPopover"
aria-label="Alert notification"
@update:model-value="onUpdate"
>
<cdx-icon :icon="cdxIconBell"></cdx-icon>
</cdx-toggle-button>
<cdx-popover
v-model:open="showPopover"
:anchor="triggerElement"
placement="bottom-start"
:render-in-place="true"
title="Alerts"
:use-close-button="true"
:icon="cdxIconBell"
:primary-action="primaryAction"
:default-action="defaultAction"
:stacked-actions="true"
@primary="showPopover = false"
@default="showPopover = false"
>
2 alerts from Wikitech and MediaWiki.
</cdx-popover>
</template>
<script>
const { defineComponent, ref } = require( 'vue' );
const { CdxPopover, CdxToggleButton, CdxIcon } = require( '@wikimedia/codex' );
const { cdxIconBell } = require( './icons.json' );
module.exports = defineComponent( {
name: 'PopoverStackedActions',
components: {
CdxPopover,
CdxToggleButton,
CdxIcon
},
setup() {
// Template ref for the Popover's trigger element needed to properly position the Popover.
const triggerElement = ref();
// Toggle the button's state and popover visibility.
const showPopover = ref( false );
// When the toggle button's state changes, an event is emitted to update
// the state in the parent component.
const onUpdate = function ( value ) {
// eslint-disable-next-line no-console
console.log( 'update:modelValue event emitted with value: ' + value );
};
// Footer action buttons.
const defaultAction = { label: 'Cancel' };
const primaryAction = { label: 'View notifications', actionType: 'progressive' };
return {
showPopover,
onUpdate,
triggerElement,
cdxIconBell,
defaultAction,
primaryAction
};
}
} );
</script>Developer notes
Use the stackedActions prop to force the action buttons to be stacked vertically, even on wide screens.
Article preview
This example uses the hover trigger since a link leads to a new page or section.
Use images and other media as needed to convey visual information in the Popover.
Use
hoveras a trigger only for elements which have a separatepressaction, such as a link.
Did you know?
- ... that in 1994 Kazuyoshi Akiyama conducted the Tokyo Symphony Orchestra in the first performance of Schoenberg's Moses und Aron with Japanese musicians?
- ... that the impact of the Charlottetown meteorite was the first to be recorded on video and audio?
- ... that the Fun Lounge police raid is considered the main cause for the formation of Mattachine Midwest , a gay rights group in Chicago?
- ... that the 8-Bit Big Band won Nintendo their first Grammy Award?
- ... that a person required intensive care after being splashed with salt water by a beluga whale?
- ... that Alia Fischer led the first women's college basketball team to achieve back-to-back undefeated seasons?
<template>
<h1>Did you know?</h1>
<ul>
<li>
... that in 1994
<a
:ref="el => triggerElements[ 0 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Kazuyoshi_Akiyama"
title="Kazuyoshi Akiyama"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Kazuyoshi Akiyama
</a>
conducted the
<a
:ref="el => triggerElements[ 1 ] = el"
class="cdx-docs-link"
href="https://en.wikipedia.org/wiki/Tokyo_Symphony_Orchestra"
title="Tokyo Symphony Orchestra"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Tokyo Symphony Orchestra
</a>
in the first performance of Schoenberg's
<a
:ref="el => triggerElements[ 2 ] = el"
class="cdx-docs-link cdx-docs-link-italic"
href="https://en.wikipedia.org/wiki/Moses_und_Aron"
title="Moses und Aron"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Moses und Aron
</a>
with Japanese musicians?
</li>
<li>
... that the impact of the
<a
:ref="el => triggerElements[ 3 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Charlottetown_meteorite"
title="Charlottetown meteorite"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Charlottetown meteorite
</a>
was the first to be recorded on video and audio?
</li>
<li>
... that the
<a
:ref="el => triggerElements[ 4 ] = el"
class="cdx-docs-link"
href="https://en.wikipedia.org/wiki/Fun_Lounge_police_raid"
title="Fun Lounge police raid"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Fun Lounge police raid
</a>
is considered the main cause for the formation of
<a
:ref="el => triggerElements[ 5 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Mattachine_Midwest"
title="Mattachine Midwest"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Mattachine Midwest
</a>
, a gay rights group in Chicago?
</li>
<li>
... that
<a
:ref="el => triggerElements[ 6 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/The_8-Bit_Big_Band"
title="The 8-Bit Big Band"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
the 8-Bit Big Band
</a>
won Nintendo their first Grammy Award?
</li>
<li>
... that a person required intensive care after being
<a
:ref="el => triggerElements[ 7 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Salt_water_aspiration_syndrome"
title="Salt water aspiration syndrome"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
splashed with salt water
</a>
by a beluga whale?
</li>
<li>
... that
<a
:ref="el => triggerElements[ 8 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Alia_Fischer"
title="Alia Fischer"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Alia Fischer
</a>
led the first women's college basketball team to achieve back-to-back
undefeated seasons?
</li>
</ul>
<cdx-popover
v-model:open="isOpen"
:anchor="currentTrigger"
:render-in-place="true"
:title="currentTitle"
>
{{ currentPreview }}
</cdx-popover>
</template>
<script>
import { defineComponent, ref } from 'vue';
import { CdxPopover } from '@wikimedia/codex';
import articlePreviews from './article-previews.json';
export default defineComponent( {
name: 'PopoverArticlePreview',
components: {
CdxPopover
},
setup() {
// Provide a template ref for the trigger element to properly position the Popover.
// Store multiple template refs for anchor `<a>` elements.
const triggerElements = ref( [] );
// Currently hovered anchor element.
const currentTrigger = ref( null );
const currentTitle = ref( '' );
const currentPreview = ref( '' );
// Control popover's visibility.
const isOpen = ref( false );
// Open the Popover on hover/focus.
const openPopover = function ( event ) {
// eslint-disable-next-line no-console
console.log( 'The title hovered or focused is:', event.target.title );
const title = event.target.title;
const foundArticle = articlePreviews.find( ( article ) => article.title === title );
currentTrigger.value = event.target;
currentTitle.value = foundArticle.title;
currentPreview.value = foundArticle.preview;
isOpen.value = true;
};
const closePopover = function () {
isOpen.value = false;
};
return {
isOpen,
triggerElements,
openPopover,
closePopover,
currentTitle,
currentPreview,
currentTrigger
};
}
} );
</script>
<style lang="less" scoped>
// Note: you must import the design tokens before importing the link mixin
@import ( reference ) '@wikimedia/codex-design-tokens/theme-wikimedia-ui.less';
@import ( reference ) '@wikimedia/codex/mixins/link.less';
.cdx-docs-link {
.cdx-mixin-link();
&-bold {
font-weight: @font-weight-bold;
}
&-italic {
/* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
font-style: italic;
}
}
h1 {
margin: @spacing-50 0;
border: @border-base;
padding: @spacing-12 @spacing-35;
font-size: @font-size-x-large;
font-weight: @font-weight-bold;
}
</style><template>
<h1>Did you know?</h1>
<ul>
<li>
... that in 1994
<a
:ref="el => triggerElements[ 0 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Kazuyoshi_Akiyama"
title="Kazuyoshi Akiyama"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Kazuyoshi Akiyama
</a>
conducted the
<a
:ref="el => triggerElements[ 1 ] = el"
class="cdx-docs-link"
href="https://en.wikipedia.org/wiki/Tokyo_Symphony_Orchestra"
title="Tokyo Symphony Orchestra"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Tokyo Symphony Orchestra
</a>
in the first performance of Schoenberg's
<a
:ref="el => triggerElements[ 2 ] = el"
class="cdx-docs-link cdx-docs-link-italic"
href="https://en.wikipedia.org/wiki/Moses_und_Aron"
title="Moses und Aron"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Moses und Aron
</a>
with Japanese musicians?
</li>
<li>
... that the impact of the
<a
:ref="el => triggerElements[ 3 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Charlottetown_meteorite"
title="Charlottetown meteorite"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Charlottetown meteorite
</a>
was the first to be recorded on video and audio?
</li>
<li>
... that the
<a
:ref="el => triggerElements[ 4 ] = el"
class="cdx-docs-link"
href="https://en.wikipedia.org/wiki/Fun_Lounge_police_raid"
title="Fun Lounge police raid"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Fun Lounge police raid
</a>
is considered the main cause for the formation of
<a
:ref="el => triggerElements[ 5 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Mattachine_Midwest"
title="Mattachine Midwest"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Mattachine Midwest
</a>
, a gay rights group in Chicago?
</li>
<li>
... that
<a
:ref="el => triggerElements[ 6 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/The_8-Bit_Big_Band"
title="The 8-Bit Big Band"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
the 8-Bit Big Band
</a>
won Nintendo their first Grammy Award?
</li>
<li>
... that a person required intensive care after being
<a
:ref="el => triggerElements[ 7 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Salt_water_aspiration_syndrome"
title="Salt water aspiration syndrome"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
splashed with salt water
</a>
by a beluga whale?
</li>
<li>
... that
<a
:ref="el => triggerElements[ 8 ] = el"
class="cdx-docs-link cdx-docs-link-bold"
href="https://en.wikipedia.org/wiki/Alia_Fischer"
title="Alia Fischer"
@mouseover="openPopover"
@focus="openPopover"
@mouseleave="closePopover"
@blur="closePopover"
>
Alia Fischer
</a>
led the first women's college basketball team to achieve back-to-back
undefeated seasons?
</li>
</ul>
<cdx-popover
v-model:open="isOpen"
:anchor="currentTrigger"
:render-in-place="true"
:title="currentTitle"
>
{{ currentPreview }}
</cdx-popover>
</template>
<script>
const { defineComponent, ref } = require( 'vue' );
const { CdxPopover } = require( '@wikimedia/codex' );
const articlePreviews = require( './article-previews.json' );
module.exports = defineComponent( {
name: 'PopoverArticlePreview',
components: {
CdxPopover
},
setup() {
// Provide a template ref for the trigger element to properly position the Popover.
// Store multiple template refs for anchor `<a>` elements.
const triggerElements = ref( [] );
// Currently hovered anchor element.
const currentTrigger = ref( null );
const currentTitle = ref( '' );
const currentPreview = ref( '' );
// Control popover's visibility.
const isOpen = ref( false );
// Open the Popover on hover/focus.
const openPopover = function ( event ) {
// eslint-disable-next-line no-console
console.log( 'The title hovered or focused is:', event.target.title );
const title = event.target.title;
const foundArticle = articlePreviews.find( ( article ) => article.title === title );
currentTrigger.value = event.target;
currentTitle.value = foundArticle.title;
currentPreview.value = foundArticle.preview;
isOpen.value = true;
};
const closePopover = function () {
isOpen.value = false;
};
return {
isOpen,
triggerElements,
openPopover,
closePopover,
currentTitle,
currentPreview,
currentTrigger
};
}
} );
</script>
<style lang="less" scoped>
// Note: you must import the design tokens before importing the link mixin
@import 'mediawiki.skin.variables.less';
@import ( reference ) '@wikimedia/codex/mixins/link.less';
.cdx-docs-link {
.cdx-mixin-link();
&-bold {
font-weight: @font-weight-bold;
}
&-italic {
/* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
font-style: italic;
}
}
h1 {
margin: @spacing-50 0;
border: @border-base;
padding: @spacing-12 @spacing-35;
font-size: @font-size-x-large;
font-weight: @font-weight-bold;
}
</style>Developer notes
The example has multiple anchor elements that displays Popover content when hovered or focused. The trigger element and Popover content is dynamically updated based on where the event took place.
- Assign each trigger element e.g. anchor element a template ref, and store them in the
triggerElementsarray. - To show and hide the Popover on hover and focus, add
mouseover,focus,mouseleave, andblurevent listeners to the anchor elements that trigger a Popover.
Bottom sheet (mobile)
The Popover component can be configured to display as a bottom sheet on mobile devices. This provides a better mobile experience with touch-friendly interactions, keyboard awareness, and safe area support.
Use the bottom sheet variant on mobile devices when the popover content is substantial or requires user interaction, such as forms or multi-step workflows.
The bottom sheet adapts to its content size, only expanding to full viewport height when necessary. Content will scroll when it exceeds the available space.
Use a backdrop/scrim to clearly separate the bottom sheet from the underlying content.
<template>
<div class="popover-bottom-sheet-demo">
<div class="popover-bottom-sheet-demo__buttons">
<cdx-button
ref="triggerButtonFull"
@click="showPopoverFull = !showPopoverFull"
>
Open Bottom Sheet with long content
</cdx-button>
<cdx-button
ref="triggerButtonMinimal"
@click="showPopoverMinimal = !showPopoverMinimal"
>
Open Bottom Sheet with minimal content
</cdx-button>
</div>
<!-- Full bottom sheet with lorem ipsum content -->
<cdx-popover
v-model:open="showPopoverFull"
use-bottom-sheet
:anchor="triggerButtonFull"
:render-in-place="true"
title="Bottom Sheet with long content"
:use-close-button="true"
:primary-action="primaryAction"
:default-action="defaultAction"
:stacked-actions="true"
@default="showPopoverFull = false"
@primary="onPrimary"
>
<template #default>
<div class="popover-bottom-sheet-demo__content">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab
illo inventore veritatis et quasi architecto beatae vitae dicta sunt
explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</p>
<cdx-field>
<cdx-label>Input Field</cdx-label>
<cdx-text-input
v-model="inputValue"
placeholder="Enter some text"
/>
</cdx-field>
<cdx-field>
<cdx-label>Input Field 2</cdx-label>
<cdx-text-input
v-model="inputValue2"
placeholder="Enter some text here"
/>
</cdx-field>
<p>
Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit, sed quia non numquam eius modi tempora
incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut
enim ad minima veniam, quis nostrum exercitationem ullam corporis
suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.
</p>
<p>
At vero eos et accusamus et iusto odio dignissimos ducimus qui
blanditiis praesentium voluptatum deleniti atque corrupti quos dolores
et quas molestias excepturi sint occaecati cupiditate non provident,
similique sunt in culpa qui officia deserunt mollitia animi, id est
laborum et dolorum fuga.
</p>
<p>
Et harum quidem rerum facilis est et expedita distinctio. Nam libero
tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo
minus id quod maxime placeat facere possimus, omnis voluptas assumenda
est, omnis dolor repellendus.
</p>
</div>
</template>
</cdx-popover>
<!-- Minimal bottom sheet -->
<cdx-popover
v-model:open="showPopoverMinimal"
use-bottom-sheet
:anchor="triggerButtonMinimal"
:render-in-place="true"
title="Bottom Sheet with minimal content"
:use-close-button="true"
:primary-action="primaryAction"
:default-action="defaultAction"
:stacked-actions="true"
@default="showPopoverMinimal = false"
@primary="onPrimaryMinimal"
>
<template #default>
<p>This is a minimal bottom sheet example with simple content.</p>
</template>
</cdx-popover>
</div>
</template>
<script>
import { defineComponent, ref, useTemplateRef } from 'vue';
import { CdxPopover, CdxButton, CdxField, CdxLabel, CdxTextInput } from '@wikimedia/codex';
export default defineComponent( {
name: 'PopoverBottomSheet',
components: {
CdxPopover,
CdxButton,
CdxField,
CdxLabel,
CdxTextInput
},
setup() {
const triggerButtonFull = useTemplateRef( 'triggerButtonFull' );
const triggerButtonMinimal = useTemplateRef( 'triggerButtonMinimal' );
const showPopoverFull = ref( false );
const showPopoverMinimal = ref( false );
const inputValue = ref( '' );
const inputValue2 = ref( '' );
const primaryAction = {
label: 'Save',
actionType: 'progressive'
};
const defaultAction = {
label: 'Cancel'
};
function onPrimary() {
// eslint-disable-next-line no-console
console.log( 'Primary action clicked of bottom sheet with long content' );
showPopoverFull.value = false;
}
function onPrimaryMinimal() {
// eslint-disable-next-line no-console
console.log( 'Primary action clicked of bottom sheet with minimal content' );
showPopoverMinimal.value = false;
}
return {
triggerButtonFull,
triggerButtonMinimal,
showPopoverFull,
showPopoverMinimal,
inputValue,
inputValue2,
primaryAction,
defaultAction,
onPrimary,
onPrimaryMinimal
};
}
} );
</script>
<style lang="less">
@import (reference) '@wikimedia/codex-design-tokens/theme-wikimedia-ui.less';
.popover-bottom-sheet-demo {
&__buttons {
display: flex;
flex-wrap: wrap;
gap: @spacing-100;
}
&__content {
p {
margin: 0 0 @spacing-100;
}
}
}
</style><template>
<div class="popover-bottom-sheet-demo">
<div class="popover-bottom-sheet-demo__buttons">
<cdx-button
ref="triggerButtonFull"
@click="showPopoverFull = !showPopoverFull"
>
Open Bottom Sheet with long content
</cdx-button>
<cdx-button
ref="triggerButtonMinimal"
@click="showPopoverMinimal = !showPopoverMinimal"
>
Open Bottom Sheet with minimal content
</cdx-button>
</div>
<!-- Full bottom sheet with lorem ipsum content -->
<cdx-popover
v-model:open="showPopoverFull"
use-bottom-sheet
:anchor="triggerButtonFull"
:render-in-place="true"
title="Bottom Sheet with long content"
:use-close-button="true"
:primary-action="primaryAction"
:default-action="defaultAction"
:stacked-actions="true"
@default="showPopoverFull = false"
@primary="onPrimary"
>
<template #default>
<div class="popover-bottom-sheet-demo__content">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab
illo inventore veritatis et quasi architecto beatae vitae dicta sunt
explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</p>
<cdx-field>
<cdx-label>Input Field</cdx-label>
<cdx-text-input
v-model="inputValue"
placeholder="Enter some text"
></cdx-text-input>
</cdx-field>
<cdx-field>
<cdx-label>Input Field 2</cdx-label>
<cdx-text-input
v-model="inputValue2"
placeholder="Enter some text here"
></cdx-text-input>
</cdx-field>
<p>
Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit, sed quia non numquam eius modi tempora
incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut
enim ad minima veniam, quis nostrum exercitationem ullam corporis
suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.
</p>
<p>
At vero eos et accusamus et iusto odio dignissimos ducimus qui
blanditiis praesentium voluptatum deleniti atque corrupti quos dolores
et quas molestias excepturi sint occaecati cupiditate non provident,
similique sunt in culpa qui officia deserunt mollitia animi, id est
laborum et dolorum fuga.
</p>
<p>
Et harum quidem rerum facilis est et expedita distinctio. Nam libero
tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo
minus id quod maxime placeat facere possimus, omnis voluptas assumenda
est, omnis dolor repellendus.
</p>
</div>
</template>
</cdx-popover>
<!-- Minimal bottom sheet -->
<cdx-popover
v-model:open="showPopoverMinimal"
use-bottom-sheet
:anchor="triggerButtonMinimal"
:render-in-place="true"
title="Bottom Sheet with minimal content"
:use-close-button="true"
:primary-action="primaryAction"
:default-action="defaultAction"
:stacked-actions="true"
@default="showPopoverMinimal = false"
@primary="onPrimaryMinimal"
>
<template #default>
<p>This is a minimal bottom sheet example with simple content.</p>
</template>
</cdx-popover>
</div>
</template>
<script>
const { defineComponent, ref, useTemplateRef } = require( 'vue' );
const { CdxPopover, CdxButton, CdxField, CdxLabel, CdxTextInput } = require( '@wikimedia/codex' );
module.exports = defineComponent( {
name: 'PopoverBottomSheet',
components: {
CdxPopover,
CdxButton,
CdxField,
CdxLabel,
CdxTextInput
},
setup() {
const triggerButtonFull = useTemplateRef( 'triggerButtonFull' );
const triggerButtonMinimal = useTemplateRef( 'triggerButtonMinimal' );
const showPopoverFull = ref( false );
const showPopoverMinimal = ref( false );
const inputValue = ref( '' );
const inputValue2 = ref( '' );
const primaryAction = {
label: 'Save',
actionType: 'progressive'
};
const defaultAction = {
label: 'Cancel'
};
function onPrimary() {
// eslint-disable-next-line no-console
console.log( 'Primary action clicked of bottom sheet with long content' );
showPopoverFull.value = false;
}
function onPrimaryMinimal() {
// eslint-disable-next-line no-console
console.log( 'Primary action clicked of bottom sheet with minimal content' );
showPopoverMinimal.value = false;
}
return {
triggerButtonFull,
triggerButtonMinimal,
showPopoverFull,
showPopoverMinimal,
inputValue,
inputValue2,
primaryAction,
defaultAction,
onPrimary,
onPrimaryMinimal
};
}
} );
</script>
<style lang="less">
@import 'mediawiki.skin.variables.less';
.popover-bottom-sheet-demo {
&__buttons {
display: flex;
flex-wrap: wrap;
gap: @spacing-100;
}
&__content {
p {
margin: 0 0 @spacing-100;
}
}
}
</style>Developer notes
To enable the bottom sheet variant, set the use-bottom-sheet prop to true. The bottom sheet will automatically appear on mobile devices (≤639px) and the regular popover will appear on larger screens.
- The
hide-backdropprop controls whether a backdrop/scrim is hidden (defaults tofalse, so the backdrop is shown by default) - The bottom sheet adapts to its content size, only expanding to full viewport height when necessary (accounting for safe areas)
- The bottom sheet automatically handles IOS keyboard visibility and adjusts its position accordingly
- Safe area insets are automatically applied for device notches and home indicators
- When content exceeds the available height, scrolling is automatically enabled
Testing on mobile
To test the bottom sheet functionality, resize your browser window to mobile size (≤639px) or use your browser's device emulation tools. The bottom sheet will only appear on mobile breakpoints when use-bottom-sheet is enabled.
Technical implementation
Vue usage
Popover and <teleport>
Popovers rely on Vue's built-in <teleport> feature. By default, Popovers will be teleported to the <body> element on the page, but this can be changed using Vue's provide/inject feature, with provide( 'CdxTeleportTarget', '#my-teleport-target' ). If Popover is being used with SSR, a dedicated teleport target should be provided.
Popover teleportation can be disabled by setting the renderInPlace prop.
Styling content in teleported Popovers
When a Popover is teleported (which is the default unless the renderInPlace prop is set), its contents will not be descendants of the element that contains the <cdx-popover> tag. When styling the contents of a Dialog, be careful not to use CSS selectors that assume the Dialog is inside its parent component.
For example, CSS selectors like .my-component .cdx-popover or .my-component .something-inside-the-popover won't work. Instead, set e.g. class="my-component-popover" on the <cdx-popover> tag, and use that class to style the dialog and things inside it.
Props
| Prop name | Description | Type | Default |
|---|---|---|---|
anchor | The triggering element that opens and closes the popover. This should be a template ref, which can be either an HTML element or a Vue component. This must be provided so the popover can be positioned relative to the triggering element (floating mode). Optional when only the bottom sheet variant is used on mobile. | HTMLElement|ComponentPublicInstance|null | null |
open | Whether the popover is visible. Should be provided via a v-model:open binding in the parent scope. | boolean | false |
title | Title text at the top of the popover. | string | '' |
icon | Icon displayed at the start of the popover. | Icon | '' |
useCloseButton | Add an icon-only close button to the popover header. | boolean | false |
closeButtonLabel | Visually-hidden label text for the icon-only close button in the header. Omit this prop to use the default value, "Close". | string | 'Close' |
primaryAction | Primary user action. This will display a primary button with the specified action (progressive or destructive). | PrimaryModalAction | null |
defaultAction | Default user action. This will display a normal button. | ModalAction | null |
stackedActions | Whether action buttons should be vertically stacked and 100% width. On mobile, the action buttons are stacked vertically by default. | boolean | false |
renderInPlace | Whether to disable the use of teleport and render the Popover in its original location in the document. | boolean | false |
placement | Positioning options for the Popover (floating mode only). | Placement | 'bottom' |
useBottomSheet | Whether to use the bottom sheet variant on mobile devices. When true, the popover will render as a bottom sheet on mobile breakpoints. | boolean | false |
hideBackdrop | Whether to hide the backdrop/scrim behind the bottom sheet. Only applies when useBottomSheet is true and the bottom sheet layout is active. | boolean | false |
Events
| Event name | Properties | Description |
|---|---|---|
primary | When the primary action button is clicked. | |
default | When the default action button is clicked. | |
update:open | newValue boolean - The new open/close state (true for open, false for closed) | When the open/close state changes, e.g. when the close button is clicked. |
Slots
| Name | Description | Bindings |
|---|---|---|
| header | Customizable Popover header. | |
| default | Popover body content. | |
| footer | Customizable Popover footer. |
Keyboard navigation
| Key | Function |
|---|---|
| Tab | It moves the focus to the next interactive element in tab order within the Popover. |
| Shift + Tab | It moves the focus to the previous interactive element within the Popover. |
| Esc | It closes the Popover. |