Skip to content
API 风格:

Popover ​

A rich floating panel for titles, copy, and interactive content — heavier than a Tooltip. The trigger lives in the default slot; the panel body comes from the content slot. With the click trigger the panel is a focus-trapped role="dialog"; hover and focus triggers display content without disrupting keyboard flow.

Basic usage ​

Click trigger
vue
<script setup lang="ts">
</script>

<template>
  <LhPopover title="Note" placement="top">
    <LhButton>Click me</LhButton>
    <template #content>
      <p>Rich content: any markup works here.</p>
    </template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <LhPopover title="Note" placement="top">
    <LhButton>Click me</LhButton>
    <template #content>
      <p>Rich content: any markup works here.</p>
    </template>
  </LhPopover>
</template>

Triggers ​

hover / focus / manual
vue
<template>
  <!-- Hover trigger -->
  <LhPopover trigger="hover" title="Hover" :show-after="0">
    <LhButton>Hover me</LhButton>
    <template #content><p>Moving into the panel does not close it.</p></template>
  </LhPopover>
  <!-- Focus trigger -->
  <LhPopover trigger="focus" title="Focus" :show-after="0">
    <LhInput placeholder="Focus the input" />
    <template #content><p>Shown when the input is focused.</p></template>
  </LhPopover>
  <!-- Manual trigger -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="Manual">
    <LhButton @click="manualVisible = !manualVisible">Toggle</LhButton>
    <template #content><p>Visibility is fully controlled by v-model.</p></template>
  </LhPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const manualVisible = ref(false)
</script>
vue
<template>
  <!-- Hover trigger -->
  <LhPopover trigger="hover" title="Hover" :show-after="0">
    <LhButton>Hover me</LhButton>
    <template #content><p>Moving into the panel does not close it.</p></template>
  </LhPopover>
  <!-- Focus trigger -->
  <LhPopover trigger="focus" title="Focus" :show-after="0">
    <LhInput placeholder="Focus the input" />
    <template #content><p>Shown when the input is focused.</p></template>
  </LhPopover>
  <!-- Manual trigger -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="Manual">
    <LhButton @click="manualVisible = !manualVisible">Toggle</LhButton>
    <template #content><p>Visibility is fully controlled by v-model.</p></template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return { manualVisible: false }
  },
}
</script>

Twelve placements ​

placement
vue
<template>
  <LhPopover
    v-for="p in placements"
    :key="p"
    :placement="p"
    :title="p"
    trigger="hover"
    :show-after="0"
  >
    <LhButton size="small">{{ p }}</LhButton>
    <template #content><p>Placement: {{ p }}</p></template>
  </LhPopover>
</template>

<script setup lang="ts">
const placements = [
  'top', 'top-start', 'top-end',
  'bottom', 'bottom-start', 'bottom-end',
  'left', 'left-start', 'left-end',
  'right', 'right-start', 'right-end',
] as const
</script>
vue
<template>
  <LhPopover
    v-for="p in placements"
    :key="p"
    :placement="p"
    :title="p"
    trigger="hover"
    :show-after="0"
  >
    <LhButton size="small">{{ p }}</LhButton>
    <template #content><p>Placement: {{ p }}</p></template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return {
      placements: [
        'top', 'top-start', 'top-end',
        'bottom', 'bottom-start', 'bottom-end',
        'left', 'left-start', 'left-end',
        'right', 'right-start', 'right-end',
      ],
    }
  },
}
</script>

When the viewport runs out of room the panel flips its side (flip) and slides back onto the cross axis (shift); the arrow follows the effective placement.

Interactive content ​

Quick note
vue
<template>
  <LhPopover placement="right" title="Quick note">
    <LhButton>Add note</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="Note text" />
        <LhButton type="primary" @click="saveNote">Save</LhButton>
      </div>
    </template>
  </LhPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const note = ref('')
const saved = ref('')
function saveNote() {
  saved.value = note.value
  note.value = ''
}
</script>
vue
<template>
  <LhPopover placement="right" title="Quick note">
    <LhButton>Add note</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="Note text" />
        <LhButton type="primary" @click="saveNote">Save</LhButton>
      </div>
    </template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return { note: '', saved: '' }
  },
  methods: {
    saveNote() {
      this.saved = this.note
      this.note = ''
    },
  },
}
</script>

More Props ​

disabled / arrow / width / hideAfter / popperClass / teleportTarget
Teleport host (#demo-popover-host)
vue
<script setup lang="ts">
</script>

<template>
  <!-- Disabled -->
  <LhPopover disabled title="Disabled">
    <LhButton disabled>Disabled trigger</LhButton>
    <template #content><p>With disabled, neither the trigger nor the panel responds.</p></template>
  </LhPopover>
  <!-- No arrow -->
  <LhPopover title="No arrow" :arrow="false" :show-after="0">
    <LhButton>No arrow</LhButton>
    <template #content><p>With arrow set to false the pointing arrow is not rendered.</p></template>
  </LhPopover>
  <!-- Fixed width -->
  <LhPopover title="Fixed width" width="240" :show-after="0">
    <LhButton>Fixed width</LhButton>
    <template #content><p>width sets the panel width — 240px here.</p></template>
  </LhPopover>
  <!-- Delayed hide -->
  <LhPopover title="Delayed hide" :hide-after="800" :show-after="0">
    <LhButton>Delayed hide</LhButton>
    <template #content><p>The panel collapses 800ms after the pointer leaves the trigger.</p></template>
  </LhPopover>
  <!-- Custom class -->
  <LhPopover title="Custom class" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>Custom class</LhButton>
    <template #content><p>popperClass lands on the panel root element for custom styling.</p></template>
  </LhPopover>
  <!-- Teleport target -->
  <LhPopover title="Teleport target" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Teleport host</LhButton>
    <template #content><p>The panel is teleported into the host below instead of body.</p></template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <!-- Disabled -->
  <LhPopover disabled title="Disabled">
    <LhButton disabled>Disabled trigger</LhButton>
    <template #content><p>With disabled, neither the trigger nor the panel responds.</p></template>
  </LhPopover>
  <!-- No arrow -->
  <LhPopover title="No arrow" :arrow="false" :show-after="0">
    <LhButton>No arrow</LhButton>
    <template #content><p>With arrow set to false the pointing arrow is not rendered.</p></template>
  </LhPopover>
  <!-- Fixed width -->
  <LhPopover title="Fixed width" width="240" :show-after="0">
    <LhButton>Fixed width</LhButton>
    <template #content><p>width sets the panel width — 240px here.</p></template>
  </LhPopover>
  <!-- Delayed hide -->
  <LhPopover title="Delayed hide" :hide-after="800" :show-after="0">
    <LhButton>Delayed hide</LhButton>
    <template #content><p>The panel collapses 800ms after the pointer leaves the trigger.</p></template>
  </LhPopover>
  <!-- Custom class -->
  <LhPopover title="Custom class" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>Custom class</LhButton>
    <template #content><p>popperClass lands on the panel root element for custom styling.</p></template>
  </LhPopover>
  <!-- Teleport target -->
  <LhPopover title="Teleport target" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Teleport host</LhButton>
    <template #content><p>The panel is teleported into the host below instead of body.</p></template>
  </LhPopover>
</template>

Controlled visibility ​

v-model:visible controls the popup visibility manually.

Controlled visibility
vue
<script setup lang="ts">
import { ref } from 'vue'
const popVisible = ref(false)
</script>

<template>
  <LhPopover v-model:visible="popVisible" title="Controlled popover" trigger="manual">
    <LhButton>{{ popVisible ? 'Collapse' : 'Expand' }}</LhButton>
  </LhPopover>
</template>
vue
<script>
export default {
  data() {
    return { popVisible: false }
  },
}
</script>

<template>
  <LhPopover v-model:visible="popVisible" title="Controlled popover" trigger="manual">
    <LhButton>{{ popVisible ? 'Collapse' : 'Expand' }}</LhButton>
  </LhPopover>
</template>

Popover API ​

Props ​

PropDescriptionTypeDefault
placementPlacement"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end"'top'
triggerTrigger mode"click" | "hover" | "focus" | "manual"'click'
titleTitle (or the title slot)string—
visibleVisibility for manual control (v-model:visible)boolean—
disabledDisable the triggerboolean—
arrowShow the arrowbooleantrue
widthPanel widthstring | number—
popperClassCustom panel classstring—
teleportTargetTeleport target (selector or element)string | HTMLElement'body'
showAfterShow delay (ms)number—
hideAfterHide delay (ms)number—
themeVarsComponent-level theme variables, overriding the ConfigProvider configuration{ [x: string]: string; }—
bgPatternComponent-level background pattern, overriding the ConfigProvider configurationstring—
sxsx layout props (applied to the trigger root)LhSxProps—

Events ​

EventDescriptionArgs
update:visibleVisibility changed(value: boolean)
showAfter showing—
hideAfter hiding—

Slots ​

SlotDescription
defaultTrigger content
titleCustom title
contentPanel content

Methods ​

MethodDescription
showShow programmatically
hideHide programmatically
toggleToggle programmatically

Accessibility ​

  • With the click trigger the panel is role="dialog" + aria-modal="true"; the trigger carries aria-haspopup / aria-expanded
  • Focus moves to the first focusable element in the panel, Tab cycles inside it, and focus returns to the trigger on close
  • hover / focus triggers never move focus, so keyboard usage is not interrupted
  • Escape closes the panel, as does a click outside; the title is linked via aria-labelledby
  • Positioning is a self-contained pure-function engine (flip / shift) with zero third-party runtime dependencies

The panel collapses once its trigger scrolls out of view. The panel is fixed positioned, so after the trigger leaves the viewport it would otherwise keep floating over page content at a high z-index. Every trigger mode (including manual) therefore closes the panel when the trigger fully scrolls away, emitting update:visible so external state stays in sync.

Type Definitions ​

The TypeScript type structures exported by this component (auto-extracted from packages/components/popover/src/types.ts and @lh-ui/share/core/* sources; click each entry to expand; see the Props table above for attribute descriptions):

LhPopoverProps
ts
export interface LhPopoverProps {
  placement?: LhPlacement
  trigger?: LhPopoverTrigger
  title?: string
  visible?: boolean
  disabled?: boolean
  arrow?: boolean
  width?: string | number
  popperClass?: string
  teleportTarget?: string | HTMLElement
  showAfter?: number
  hideAfter?: number
  themeVars?: Record<string, string>
  bgPattern?: string
  sx?: LhSxProps
}
LhPopoverPlacement (from @lh-ui/share/core/shared/popper)
ts

export type LhPlacement =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'
  | 'left'
  | 'left-start'
  | 'left-end'
  | 'right'
  | 'right-start'
  | 'right-end'
LhPopoverTrigger (from @lh-ui/share/core/popover (logic.ts))
ts
export type LhPopoverTrigger = 'click' | 'hover' | 'focus' | 'manual'

Beauty of tradition, foundation of code