Skip to content

Popover

Popover can be used for dropdowns and other popovers when an element is clicked, focused or hovered. You have the following types of trigger as an option:

  • trigger on click with command="toggle-popover" and commandfor
  • trigger on hover/focus with the interestfor attribute (see the Interest variant)

Popovers are build entirely on the web platform — the [popover] attribute, Invoker Commands (command="toggle-popover" / show-popover / hide-popover) and CSS Anchor Positioning.

No JavaScript is required in browsers with anchor positioning support.

View Source 

Installation

Follow instructions for individual framework usage below

  • winduum 
  • winduum-elements 
  • winduum-stimulus 
  • winduum-vue 
  • winduum-react 

Browser support

The Popover API is Baseline. For older browsers cover Invoker Commands with invokers-polyfill and interestfor with the interestfor polyfill. Where CSS Anchor Positioning isn't supported, use the floating-ui fallback helpers. See Polyfills.

Styles

Variants

  • default 
  • content 
  • tooltip 

Props

  • default 
  • content 

Tokens

Positioning of x-popover is provided by the Position utilities

  • top top-start top-end
  • bottom bottom-start bottom-end
  • left left-start left-end
  • right right-start right-end

Scripts

The Popover script keeps trigger aria-expanded in sync and provides the floating-ui positioning fallback when needed.

Examples

Basic Popover

Native Popover API with Invoker Commands — the popover lives in the top-layer, gets light-dismiss (Esc, clicking outside) and keyboard handling for free, without a single line of JavaScript.

html
<button class="x-button" command="toggle-popover" commandfor="popoverExample">Popover</button>
<div class="x-popover bottom my-2" popover id="popoverExample">
    <div class="x-popover-content shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
        <button class="x-button ghosted accent-main justify-start w-full">Item 1</button>
        <button class="x-button ghosted accent-main justify-start w-full">Item 2</button>
    </div>
</div>
vue
<script setup lang="ts">
    import { useId } from 'vue'
    import { Button } from '@/components/button'
    import { Popover, PopoverContent } from '@/components/popover'

    const popoverBasic = useId()
</script>

<template>
    <Button command="toggle-popover" :commandfor="popoverBasic">
        Popover
    </Button>
    <Popover class="bottom my-2" popover :id="popoverBasic">
        <PopoverContent class="shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
            <Button class="ghosted accent-main justify-start w-full">Item 1</Button>
            <Button class="ghosted accent-main justify-start w-full">Item 2</Button>
        </PopoverContent>
    </Popover>
</template>
tsx
import { useId } from 'react'
import { Button } from '@/components/button'
import { Popover, PopoverContent } from '@/components/popover'

export function Example() {
    const popoverBasic = useId()

    return (
        <>
            <Button command="toggle-popover" commandfor={popoverBasic}>
                Popover
            </Button>
            <Popover className="bottom my-2" popover="auto" id={popoverBasic}>
                <PopoverContent className="shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
                    <Button className="ghosted accent-main justify-start w-full">Item 1</Button>
                    <Button className="ghosted accent-main justify-start w-full">Item 2</Button>
                </PopoverContent>
            </Popover>
        </>
    )
}

Interest Popover

Shows the popover when the user shows interest in the trigger — hover or keyboard focus — via the interestfor attribute. Use the interestfor polyfill for browsers without native support.

html
<button class="x-button" interestfor="popoverHover">Popover</button>
<div class="x-popover bottom my-2" popover="hint" id="popoverHover">
    <div class="x-popover-content shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
        <button class="x-button ghosted accent-main justify-start w-full">Item 1</button>
        <button class="x-button ghosted accent-main justify-start w-full">Item 2</button>
    </div>
</div>
vue
<script setup lang="ts">
    import { useId } from 'vue'
    import { Button } from '@/components/button'
    import { Popover, PopoverContent } from '@/components/popover'

    const popoverInterest = useId()
</script>

<template>
    <Button :interestfor="popoverInterest">Popover</Button>
    <Popover class="bottom my-2" popover="hint" :id="popoverInterest">
        <PopoverContent class="shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
            <Button class="ghosted accent-main justify-start w-full">Item 1</Button>
            <Button class="ghosted accent-main justify-start w-full">Item 2</Button>
        </PopoverContent>
    </Popover>
</template>
tsx
import { useId } from 'react'
import { Button } from '@/components/button'
import { Popover, PopoverContent } from '@/components/popover'

export function Example() {
    const popoverInterest = useId()

    return (
        <>
            <Button interestfor={popoverInterest}>Popover</Button>
            <Popover className="bottom my-2" popover="hint" id={popoverInterest}>
                <PopoverContent className="shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
                    <Button className="ghosted accent-main justify-start w-full">Item 1</Button>
                    <Button className="ghosted accent-main justify-start w-full">Item 2</Button>
                </PopoverContent>
            </Popover>
        </>
    )
}

Positioning fallback

Same as the above examples, but wrapped as component, which applies the floating-ui positioning fallback in browsers without CSS Anchor Positioning.

html
<div class="w-full flex flex-col items-center">
    <button class="x-button" command="toggle-popover" commandfor="popoverApi">Popover</button>
    <x-popover class="x-popover bottom my-2" popover id="popoverApi" data-placement="bottom">
        <div class="x-popover-content shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
            <button class="x-button ghosted accent-main justify-start w-full">Item 1</button>
            <button class="x-button ghosted accent-main justify-start w-full">Item 2</button>
        </div>
    </x-popover>
</div>
liquid
<div class="w-full flex flex-col items-center">
    <button class="x-button" command="toggle-popover" commandfor="popoverApi">Popover</button>
    <div class="x-popover bottom my-2" popover id="popoverApi" data-controller="x-popover" data-x-popover-placement-value="bottom">
        <div class="x-popover-content shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
            <button class="x-button ghosted accent-main justify-start w-full">Item 1</button>
            <button class="x-button ghosted accent-main justify-start w-full">Item 2</button>
        </div>
    </div>
</div>
vue
<script setup>
    import { useId } from 'vue'
    import { Button } from '@/components/button'
    import { Popover, PopoverContent } from '@/components/popover'

    const popoverApi = useId()
</script>

<template>
    <div class="w-full flex flex-col items-center">
        <Button command="toggle-popover" :commandfor="popoverApi">
            Popover
        </Button>
        <Popover class="bottom my-2" popover :id="popoverApi" placement="bottom">
            <PopoverContent class="shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
                <Button class="ghosted accent-main justify-start w-full">Item 1</Button>
                <Button class="ghosted accent-main justify-start w-full">Item 2</Button>
            </PopoverContent>
        </Popover>
    </div>
</template>
tsx
import { useId } from 'react'
import { Button } from '@/components/button'
import { Popover, PopoverContent } from '@/components/popover'

export function Example() {
    const popoverApi = useId()

    return (
        <div className="w-full flex flex-col items-center">
            <Button command="toggle-popover" commandfor={popoverApi}>
                Popover
            </Button>
            <Popover className="bottom my-2" popover="auto" id={popoverApi} placement="bottom">
                <PopoverContent className="shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
                    <Button className="ghosted accent-main justify-start w-full">Item 1</Button>
                    <Button className="ghosted accent-main justify-start w-full">Item 2</Button>
                </PopoverContent>
            </Popover>
        </div>
    )
}

Tooltip Popover

Uses the tooltip variant with an interest-triggered hint popover. Setting interest-delay to 0s shows it immediately on hover or keyboard focus. The component integrations also apply the floating-ui positioning fallback in browsers without CSS Anchor Positioning.

html
<button class="x-button" interestfor="tooltipPopover" style="interest-delay: 0s;">Tooltip</button>
<x-popover class="x-popover tooltip bottom my-1" id="tooltipPopover" popover="hint" data-placement="bottom">
    <div class="x-popover-content">
        Popover
    </div>
</x-popover>
liquid
<button class="x-button" interestfor="tooltipPopover" style="interest-delay: 0s;">Tooltip</button>
<div class="x-popover tooltip bottom my-1" id="tooltipPopover" popover="hint" data-controller="x-popover" data-x-popover-placement-value="bottom">
    <div class="x-popover-content">
        Popover
    </div>
</div>
vue
<script setup lang="ts">
    import { useId } from 'vue'
    import { Button } from '@/components/button'
    import { Popover, PopoverContent } from '@/components/popover'

    const tooltipPopover = useId()
</script>

<template>
    <Button :interestfor="tooltipPopover" style="interest-delay: 0s;">
        Tooltip
    </Button>
    <Popover class="tooltip bottom my-1" :id="tooltipPopover" popover="hint" placement="bottom">
        <PopoverContent>
            Popover
        </PopoverContent>
    </Popover>
</template>
tsx
import { type CSSProperties, useId } from 'react'
import { Button } from '@/components/button'
import { Popover, PopoverContent } from '@/components/popover'

export function Example() {
    const tooltipPopover = useId()

    return (
        <>
            <Button interestfor={tooltipPopover} style={{ interestDelay: '0s' } as CSSProperties}>
                Tooltip
            </Button>
            <Popover className="tooltip bottom my-1" id={tooltipPopover} popover="hint" placement="bottom">
                <PopoverContent>
                    Popover
                </PopoverContent>
            </Popover>
        </>
    )
}

JavaScript API

The core popover component needs no JavaScript — use the native showPopover / hidePopover / togglePopover methods for programmatic control.

Two helpers are exported for the floating-ui positioning fallback (they require @floating-ui/dom as a dependency). They are used internally by winduum-elements and winduum-stimulus, but you can use them to build your own integration.

computePositionPopover

  • Type: (referenceElement: HTMLElement, floatingElement: HTMLElement, placement: Placement, options?: ComputePositionConfig | boolean) => Promise<void>
  • Kind: async

Computes the popover position with @floating-ui/dom and applies it to the floating element. When native CSS anchor positioning is supported, only the placement class is applied and positioning is left to CSS. Passing options: true enables the built-in flip() middleware.

Example

js
import { computePositionPopover } from 'winduum/src/components/popover'

const trigger = document.querySelector('#popoverTrigger')
const popover = document.querySelector('#popoverElement')

popover.addEventListener('toggle', async ({ newState }) => {
    if (newState === 'open') {
        await computePositionPopover(trigger, popover, 'bottom-start')
    }
})

autoUpdatePopover

  • Type: (referenceElement: HTMLElement, floatingElement: HTMLElement, placement: Placement, options?: ComputePositionConfig | boolean) => Promise<() => void>
  • Kind: async

Same as computePositionPopover, but keeps the position updated on scroll and resize via floating-ui autoUpdate. Returns a cleanup function — call it when the popover closes.

Example

js
import { autoUpdatePopover } from 'winduum/src/components/popover'

let cleanup

popover.addEventListener('toggle', async ({ newState }) => {
    if (newState === 'open') {
        cleanup = await autoUpdatePopover(trigger, popover, 'bottom-start')
    } else {
        cleanup?.()
    }
})

Released under the MIT License.