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
- Add `popover.anchor()` to the trigger element inside the same popover context, e.g. `mix={[popover.anchor()]}` on the button that toggles the popover
- Ensure the anchor element is rendered whenever `open` can be true — don't conditionally unmount the anchor while keeping the surface open
- 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
- Always author the anchor trigger and the surface together as one popover component so they can't diverge
- Never conditionally render the anchor while `open` is true
- Add a test that toggles open immediately after mount to catch missing-anchor regressions
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
- clientEntry() requires an entry ID
- ${name} must return a render function, received ${typeof res
- render called after component was removed, potential applica
- Duplicate keys detected in siblings: ${quotedKeys.join(', ')
- ${cascadingComponentUpdateCount} cascading component updates
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/d1aca18386af64b0.
Report an issue: GitHub.