On this pageOverview
Popover
An anchored floating panel with natural Tab navigation. Unlike Dialog (which is modal and traps focus) or Menu (which uses aria-activedescendant for item navigation), Popover holds arbitrary content and uses the disclosure ARIA pattern. Focus flows naturally through the panel content.
For programmatic control in update functions, use Popover.open(model) and Popover.close(model) which return [Model, Commands] directly.
See it in an app
Check out how Popover is wired up in a real Foldkit app.
Pass anchor to position the panel relative to the button. The panel can hold any content — links, forms, or informational text.
// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt — fit them into your own Model, init, Message,
// update, and view definitions.
import { Effect } from 'effect'
import { Command, Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, div, h3, p, span } from './html'
// Add a field to your Model for the Popover Submodel:
const Model = S.Struct({
popover: Ui.Popover.Model,
// ...your other fields
})
// In your init function, initialize the Popover Submodel with a unique id:
const init = () => [
{
popover: Ui.Popover.init({ id: 'info' }),
// ...your other fields
},
[],
]
// Embed the Popover Message in your parent Message:
const GotPopoverMessage = m('GotPopoverMessage', {
message: Ui.Popover.Message,
})
// Inside your update function's M.tagsExhaustive({...}), delegate to Popover.update:
GotPopoverMessage: ({ message }) => {
const [nextPopover, commands] = Ui.Popover.update(model.popover, message)
return [
// Merge the next state into your Model:
evo(model, { popover: () => nextPopover }),
// Forward the Submodel's Commands through your parent Message:
commands.map(
Command.mapEffect(Effect.map(message => GotPopoverMessage({ message }))),
),
]
}
// Inside your view function, render the popover:
Ui.Popover.view({
model: model.popover,
toParentMessage: message => GotPopoverMessage({ message }),
buttonContent: span([], ['Solutions']),
buttonClassName: 'rounded-lg border px-3 py-2 cursor-pointer',
panelContent: div(
[],
[
h3([Class('font-medium')], ['Analytics']),
p(
[Class('text-sm text-gray-500')],
['Get a better understanding of where your traffic is coming from.'],
),
],
),
panelClassName: 'rounded-lg border shadow-lg p-4 w-80',
backdropClassName: 'fixed inset-0',
anchor: { placement: 'bottom-start', gap: 4, padding: 8 },
})Pass isAnimated: true at init for CSS transition coordination.
Popover is headless — button and panel styling is controlled through className and attribute props.
| Attribute | Condition |
|---|---|
data-open | Present on button and panel when open. |
data-disabled | Present on the button when disabled. |
data-closed | Present during close animation. |
The panel receives tabindex="0" so it can receive focus. Tab navigates naturally through the panel content. Escape closes and returns focus to the button.
| Key | Description |
|---|---|
| Enter / Space | Toggles the popover. |
| Escape | Closes the popover and returns focus to the button. |
| Tab | Navigates within the panel. Closes the popover when focus leaves. |
The button receives aria-expanded and aria-controls linking to the panel. The panel has no role — Popover uses the disclosure pattern, not the menu pattern.
Configuration object passed to Popover.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the popover instance. |
isAnimated | boolean | false | Enables CSS transition coordination. |
isModal | boolean | false | Locks page scroll and marks other elements inert when open. |
Configuration object passed to Popover.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | Popover.Model | - | The popover state from your parent Model. |
toParentMessage | (childMessage: Popover.Message) => ParentMessage | - | Wraps Popover Messages in your parent Message type for Submodel delegation. |
buttonContent | Html | - | Content rendered inside the trigger button. |
panelContent | Html | - | Content rendered inside the floating panel. |
anchor | AnchorConfig | - | Floating positioning config: placement, gap, and padding. Required. |
buttonClassName | string | - | CSS class for the trigger button. |
buttonAttributes | ReadonlyArray<Attribute<Message>> | - | Additional attributes for the trigger button. |
panelClassName | string | - | CSS class for the floating panel. |
panelAttributes | ReadonlyArray<Attribute<Message>> | - | Additional attributes for the panel. |
backdropClassName | string | - | CSS class for the backdrop. |
isDisabled | boolean | false | Disables the trigger button. |
onOpened | () => Message | - | Optional callback fired when the popover opens. |
onClosed | () => Message | - | Optional callback fired when the popover closes. |