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.