hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn
useSafeArea
usePresence

Popover#

A non-modal, anchored overlay for interactive content: menus of actions, small forms, or detail panels. Positioning runs on Floating UI; the overlay renders in place and promotes to the browser top layer using the Popover API. It ships unstyled: style it through the data-state, data-side, and data-align contract.

Demo#

Styling#

Parts expose data-state="open" | "closed"; the Positioner and Arrow also expose data-side and data-align. The Positioner is the fixed-positioned wrapper, so size, z-index, and entry animation go on the Popup inside it. The demo above uses the styles below; copy a starting point in either flavor:

.docs-popover-positioner {
  z-index: 50;
}
.docs-popover {
  box-sizing: border-box;
  width: max(16rem, 12rem);
  padding: 1rem;
  border: 1px solid #e4e4e7;
  border-radius: 0.625rem;
  background: #fff;
  color: #18181b;
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.25),
    0 4px 6px -4px rgb(0 0 0 / 0.25);
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 120ms ease,
    transform 120ms ease;
}
/* Compact popup typography so the title/description do not inherit page heading
   and paragraph margins. */
.docs-popover h2 {
  font-size: 0.95rem;
  font-weight: 600;
  margin: 0 0 0.25rem;
}
.docs-popover p {
  margin: 0 0 0.75rem;
  font-size: 0.875rem;
  color: #71717a;
}
@starting-style {
  .docs-popover[data-state='open'] {
    opacity: 0;
    transform: translateY(-4px);
  }
}
.docs-popover[data-state='closed'] {
  animation: docs-popover-out 120ms ease-in forwards;
}
@keyframes docs-popover-out {
  to {
    opacity: 0;
    transform: translateY(-4px);
  }
}
/* The arrow is a rotated square; the component sets position:absolute and the
   cross-axis offset, the per-side rules place it on the edge facing the anchor.
   The Positioner already clears the UA top-layer styles, so nothing else is
   needed for the shadow to show. */
.docs-popover__arrow {
  width: 9px;
  height: 9px;
  rotate: 45deg;
  background: #fff;
  border: 1px solid #e4e4e7;
}
.docs-popover__arrow[data-side='bottom'] {
  top: -5px;
  border-right: 0;
  border-bottom: 0;
}
.docs-popover__arrow[data-side='top'] {
  bottom: -5px;
  border-left: 0;
  border-top: 0;
}
.docs-popover__arrow[data-side='right'] {
  left: -5px;
  border-top: 0;
  border-right: 0;
}
.docs-popover__arrow[data-side='left'] {
  right: -5px;
  border-bottom: 0;
  border-left: 0;
}
@media (prefers-color-scheme: dark) {
  .docs-popover {
    border-color: #27272a;
    background: #18181b;
    color: #fafafa;
  }
  .docs-popover p {
    color: #a1a1aa;
  }
  .docs-popover__arrow {
    background: #18181b;
    border-color: #27272a;
  }
}
@media (prefers-reduced-motion: reduce) {
  .docs-popover {
    transition: none;
  }
  .docs-popover[data-state='closed'] {
    animation: none;
  }
}

API reference#

Every part accepts a render prop for composition (see renderElement) and forwards unknown props to the element it renders. The Trigger, Positioner, Popup, Arrow, and Close also expose data-state="open" | "closed" and pass state to a render function.

Popover.Root#

Provides open state, ids, refs, and placement config to the parts. Renders only its children.

PropTypeDefaultDescription
openboolean-Controlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the popover requests an open or close.
side'top'|'right'|'bottom'|'left''bottom'Preferred side of the anchor to place the popup.
align'start'|'center'|'end''center'Alignment along that side.
offsetnumber8Gap in pixels between the anchor and the popup.
childrenComponentChildren-The trigger, optional anchor, and positioner.

Popover.Trigger#

Toggles the popover on click and anchors it (unless a Popover.Anchor is present). Default element <button type="button">.

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
childrenComponentChildren-Trigger label.
...propsJSX.HTMLAttributes<HTMLButtonElement>-Forwarded to the element; a passed onClick runs before the popover toggles.

Sets aria-haspopup="dialog", aria-expanded, id, data-state, and aria-controls (only while open, since the popup is mounted on open).

Popover.Anchor#

Optional. Positions the popover relative to this element instead of the trigger, for example to anchor a popover to a region while a separate button opens it. Default element <span>.

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
childrenComponentChildren-The anchored content.
...propsJSX.HTMLAttributes<HTMLSpanElement>-Forwarded to the element.

Popover.Positioner#

The fixed-positioned wrapper that Floating UI drives. Renders nothing until the popover is open (mount on open). Default element <div>.

PropTypeDefaultDescription
renderRenderProp<{ side: Side; align: Align }>-Compose or replace the element.
childrenComponentChildren-The popup (and optional arrow).
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element. Style positioning via class.

Sets position: fixed and the resolved data-side / data-align. The element is promoted to the top layer via the Popover API, so it escapes ancestor clipping.

Popover.Popup#

The surface and focus target. Default element <div> with role="dialog".

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
aria-labelstring-Accessible name when there is no Popover.Title.
childrenComponentChildren-Arrow, title, description, body, and controls.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets role="dialog", id, tabindex="-1", data-state, aria-labelledby (the Title) or aria-label, and aria-describedby (only when a Popover.Description is present). Registers the Escape / outside-press dismissal and the focus move-in / return.

Popover.Arrow#

Optional pointer positioned from the Floating UI arrow data. Default element <div>. Place it on the correct edge with CSS keyed on data-side.

PropTypeDefaultDescription
renderRenderProp<{ side: Side }>-Compose or replace the element.
childrenComponentChildren-Optional arrow content.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets data-side and position: absolute with the computed offset.

Popover.Title#

The popover's accessible name, wired to the popup's aria-labelledby. Default element <h2>.

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
childrenComponentChildren-Title text.
...propsJSX.HTMLAttributes<HTMLHeadingElement>-Forwarded to the element.

Popover.Description#

Optional supporting text, wired to the popup's aria-describedby while it is rendered. Default element <p>.

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
childrenComponentChildren-Description text.
...propsJSX.HTMLAttributes<HTMLParagraphElement>-Forwarded to the element.

Popover.Close#

Closes the popover on click. Default element <button type="button">.

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
childrenComponentChildren-Button label.
...propsJSX.HTMLAttributes<HTMLButtonElement>-Forwarded to the element; a passed onClick runs before the popover closes.

Primitives#

hono-preact-ui exports the building blocks the parts use, for composing your own components. Several have their own page with examples: usePosition, useDismiss, useFocusReturn, renderElement, useControllableState, and mergeRefs.

Accessibility#

The popover is a non-modal disclosure: the trigger carries aria-haspopup, aria-expanded, and (while open) aria-controls. Give the popup an accessible name with a Popover.Title (wired through aria-labelledby) or by passing aria-label to Popover.Popup; a Popover.Description is wired through aria-describedby when present.

  • Focus moves into the popup when it opens (the first focusable element, or the popup itself) and returns to the trigger when it closes.
  • Focus is not trapped: the popover is non-modal, so tabbing past the last control moves into the rest of the page, and the page stays interactive.
  • Escape closes the popover; an outside pointer press closes it; nested overlays close innermost-first.
  • The popup renders in the top layer via the Popover API, so an ancestor transform, filter, contain, or will-change does not clip it.