Skip to content

Popover

A small dialog anchored to the element that opened it.

View source View as Markdown

Popover is a small dialog anchored to the element that opened it. Use it for a share sheet, a filter panel or a short form that would be too much for a Tooltip and too little for a modal.

The example switches the teleport off, so the popup renders in place and scrolls with the frame around it rather than with the page.

App.vue
<template>
  <Popover.Root :options="options">
    <Popover.Trigger :class="trigger">
      Share
      <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
        <path
          d="M8 10.5V2m0 0L5 5m3-3 3 3M3 9.5v3.5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9.5"
          fill="none"
          stroke="currentColor"
          stroke-width="1.5"
          stroke-linecap="round"
          stroke-linejoin="round"
        />
      </svg>
    </Popover.Trigger>

    <Popover.Content :class="popup">
      <Popover.Arrow :class="arrow" />

      <div class="flex flex-col gap-1 p-3.5">
        <Popover.Title class="type-component-lg text-primary-on-subtle">
          Share this page
        </Popover.Title>

        <Popover.Description class="type-component-2xs text-primary-muted">
          Anyone with the link can read it.
        </Popover.Description>

        <div :class="field">
          <Input v-model="link" :class="control" aria-label="Link" readonly />
        </div>

        <Popover.Close :class="close">Done</Popover.Close>
      </div>
    </Popover.Content>
  </Popover.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Input } from '@maas/mirror/vue/Input'
import { Popover } from '@maas/mirror/vue/Popover'

const link = ref('https://mirror.maas.engineering/share/8f21')

const options = {
  teleport: { disabled: true },
  floating: { sideOffset: 10 },
}

const trigger = [
  'inline-flex h-12 items-center justify-center gap-1.5 px-[1.125rem] whitespace-nowrap',
  'rounded-component-lg border-2 border-[transparent] type-component-lg',
  'bg-primary-solid text-primary-on-solid',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-solid-hover active:bg-primary-solid-active',
  'data-[content-open=true]:bg-primary-solid-active',
].join(' ')

const popup = [
  'relative w-80 max-w-[calc(100vw-2rem)]',
  'rounded-component-2xl bg-primary-inverted shadow-component-high',
  'p-1.5 backdrop-blur-[8rem]',
  'outline-4 outline-transparent focus-visible:focus-ring',
  '[&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
].join(' ')

const arrow = [
  'bg-primary-inverted size-3 rotate-45',
  'data-[side=top]:top-full data-[side=top]:-mt-1.5',
  'data-[side=bottom]:bottom-full data-[side=bottom]:-mb-1.5',
  'data-[side=left]:left-full data-[side=left]:-ml-1.5',
  'data-[side=right]:right-full data-[side=right]:-mr-1.5',
].join(' ')

const field = [
  'mt-2.5 flex h-12 w-full items-center',
  'rounded-component-lg border-primary-subtle border-2 px-[0.875rem]',
  'outline-4 outline-transparent focus-within:focus-ring',
].join(' ')

const control = [
  'type-component-sm leading-[normal]! text-primary-on-subtle',
  'block h-full w-full appearance-none bg-transparent outline-none',
].join(' ')

const close = [
  'mt-1.5 inline-flex h-10 items-center justify-center px-3.5',
  'rounded-component-lg type-component-sm',
  'bg-primary-subtle text-primary-on-subtle',
  'hover:bg-primary-subtle-hover active:bg-primary-subtle-active',
  'outline-4 outline-transparent focus-visible:focus-ring',
].join(' ')
</script>

Usage guidelines

  • Give the popup a name, either through a Popover.Title or an aria-label on Popover.Content. Screen readers announce it as a dialog and read that name when it opens.must
  • A popover is not modal by default: the page stays live behind it and the popup closes when you press outside it. Set options.modal for the cases where the rest of the page has to wait.may
  • Popover.Content teleports to the end of <body> unless you turn that off with options.teleport.disabled. Leave it on where the trigger sits inside anything with overflow: hidden.should
  • The content resolves everything but side and align once, during setup, so change the rest before the popup first renders or key the root to remount it.must
  • For a label on an icon button, reach for Tooltip instead; a popover takes focus, and a tooltip must not.should