remix-run/remix · error · Error

popover.surface() requires a registered anchor before openin

Error message

popover.surface() requires a registered anchor before opening

What it means

The popover surface mixin opens the native Popover API element in response to an `open` prop, but before it can position itself it needs an anchor registered via `popover.anchor()` (which stores `{ target, options }` on the shared PopoverProvider context). When the `beforetoggle` event fires with newState 'open' and `context.anchor` is null, the mixin throws because there is no element to position the surface against.

Source

Thrown at packages/ui/src/popover/index.ts:106

      let wasOpen = openProp
      openProp = options.open

      handle.queueTask(async (node) => {
        if (openProp && !wasOpen) {
          node.showPopover()
        } else if (!openProp && wasOpen) {
          node.hidePopover()
        }
      })

      return [
        attrs({ popover: 'manual' }),

        on('beforetoggle', (event) => {
          if (event.newState === 'open') {
            let anchor = context.anchor
            if (!anchor) {
              throw new Error('popover.surface() requires a registered anchor before opening')
            }

            cleanupAnchor = positionAnchor(event.currentTarget, anchor.target, anchor.options)
            unlockScroll = lockScroll()
          } else if (event.newState === 'closed') {
            cleanupAnchor()
            unlockScroll()
          }
        }),

        on('toggle', async (event) => {
          if (event.newState === 'open') {
            context.showFocusTarget?.focus()
          } else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
            context.hideFocusTarget?.focus()
          }
        }),

View on GitHub (pinned to 9696913134)

Solutions

  1. Add `popover.anchor()` to the trigger element inside the same popover context, e.g. `mix={[popover.anchor()]}` on the button that toggles the popover
  2. Ensure the anchor element is rendered whenever `open` can be true — don't conditionally unmount the anchor while keeping the surface open
  3. Keep the anchor and surface under the same popover provider component so `handle.context.get(PopoverProvider)` resolves to the same instance

Example fix

// before
<button mix={[on('click', () => { open = true; handle.update() })]}>Menu</button>
<div mix={[popover.surface({ open, onHide })]}>…</div>

// after
<button mix={[popover.anchor(), on('click', () => { open = true; handle.update() })]}>Menu</button>
<div mix={[popover.surface({ open, onHide })]}>…</div>
Defensive patterns

Strategy: validation

Validate before calling

// Before setting open=true, ensure the anchor mixin has been applied in the same
// popover context — structurally: check that your trigger element includes
// popover.anchor() and is rendered:
if (open && !anchorMounted) {
  // render the trigger/anchor first, then open
  anchorMounted = true
  handle.update()
  handle.queueTask(() => { open = true; handle.update() })
}

Prevention

When it happens

Trigger: Rendering a `popover.surface({ open: true, ... })` element without a sibling/ancestor element that applies `popover.anchor(options)` inside the same popover provider context. Also occurs if the anchor element is conditionally not rendered (e.g. anchor branch skipped) while open is true, or the surface is used outside a popover root so context.anchor was never set.

Common situations: Copy-pasting a popover surface markup without the trigger's anchor mixin; toggling `open` to true before the anchor element mounts (conditional rendering race); refactoring so the anchor and surface no longer share the popover context provider.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/d1aca18386af64b0. Report an issue: GitHub.