๐ŸŸข Popover

  • Can be controlled or uncontrolled.
  • Customize side, alignment, offsets, collision handling.
  • Optionally render a pointing arrow.
  • Focus is fully managed and customizable.
  • Supports modal and non-modal modes.
  • Dismissing and layering behavior is highly customizable.

Basic

Displays rich content in a portal, triggered by a button.

<template>
  <div class="grid h-50 place-items-center">
    <NPopover :_popover-content="{ align: 'start', class: 'w-80' }">
      <template #trigger>
        <NButton btn="solid-white">
          Open popover
        </NButton>
      </template>
      <div class="grid gap-4">
        <div class="space-y-2">
          <h4 class="font-medium leading-none">
            Dimensions
          </h4>
          <p class="text-sm text-muted">
            Set the dimensions for the layer.
          </p>
        </div>
        <div class="grid gap-2">
          <div class="grid grid-cols-3 items-center gap-4">
            <NLabel for="width">
              Width
            </NLabel>
            <NInput
              id="width"
              model-value="100%"
              :una="{
                inputWrapper: 'col-span-2',
              }"
            />
          </div>
          <div class="grid grid-cols-3 items-center gap-4">
            <NLabel for="maxWidth">
              Max. width
            </NLabel>
            <NInput
              id="maxWidth"
              model-value="300px"
              :una="{
                inputWrapper: 'col-span-2',
              }"
            />
          </div>
          <div class="grid grid-cols-3 items-center gap-4">
            <NLabel for="height">
              Height
            </NLabel>
            <NInput
              id="height"
              type="text"
              :una="{
                inputWrapper: 'col-span-2',
              }"
            />
          </div>
          <div class="grid grid-cols-3 items-center gap-4">
            <NLabel for="maxHeight">
              Max. height
            </NLabel>
            <NInput
              id="maxHeight"
              :una="{
                inputWrapper: 'col-span-2',
              }"
            />
          </div>
        </div>
      </div>
    </NPopover>
  </div>
</template>

Slots

You can use the following slots to customize the popover.

NameDescriptionProps
triggerThe button trigger.open
defaultThe popover content.-

Props

import type { PopoverContentProps, PopoverRootProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'

interface BaseExtensions { class?: HTMLAttributes['class'] }

export interface NPopoverProps extends PopoverRootProps {
  /** Props for the popover content */
  _popoverContent?: NPopoverContentProps
}

export interface NPopoverContentProps extends PopoverContentProps, BaseExtensions {
}

Presets

type PopoverPrefix = 'popover'

export const staticPopover: Record<`${PopoverPrefix}-${string}`, string> = {
  'popover-content': 'z-50 w-72 rounded-md border border-base bg-popover p-4 text-popover shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
}

export const dynamicPopover: [RegExp, (params: RegExpExecArray) => string][] = [
  // dynamic preset
]

export const popover = [
  ...dynamicPopover,
  staticPopover,
]

Component

<script setup lang="ts">
import type { PopoverRootEmits } from 'radix-vue'
import type { NPopoverProps } from '../../../types'
import { PopoverRoot, PopoverTrigger, useForwardPropsEmits } from 'radix-vue'
import { computed } from 'vue'
import NPopoverContent from './PopoverContent.vue'

const props = defineProps<NPopoverProps>()
const emits = defineEmits<PopoverRootEmits>()

const delegatedProps = computed(() => {
  const { _popoverContent, ...delegated } = props

  return delegated
})

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
  <PopoverRoot v-slot="{ open }" v-bind="forwarded">
    <PopoverTrigger as-child>
      <slot name="trigger" :open />
    </PopoverTrigger>
    <NPopoverContent v-bind="_popoverContent">
      <slot />
    </NPopoverContent>
  </PopoverRoot>
</template>
<script setup lang="ts">
import type { NPopoverContentProps } from '../../../types'
import {
  PopoverContent,
  type PopoverContentEmits,
  PopoverPortal,
  useForwardPropsEmits,
} from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'

defineOptions({
  inheritAttrs: false,
})

const props = withDefaults(
  defineProps<NPopoverContentProps>(),
  {
    align: 'center',
    sideOffset: 4,
  },
)
const emits = defineEmits<PopoverContentEmits>()

const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props

  return delegated
})

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
  <PopoverPortal>
    <PopoverContent
      v-bind="{ ...forwarded, ...$attrs }"
      :class="
        cn(
          'popover-content',
          props.class,
        )
      "
    >
      <slot />
    </PopoverContent>
  </PopoverPortal>
</template>