Remix
Remix

remix/ui/anchor

anchor positions a floating element against an anchor element and keeps it constrained to the viewport. Use it for custom floating surfaces that need placement, flipping, offsets, and optional relative alignment.

Usage

import { anchor } from 'remix/ui/anchor'

let trigger = document.querySelector<HTMLButtonElement>('[data-trigger]')
let panel = document.querySelector<HTMLElement>('[data-panel]')

if (trigger && panel) {
  let cleanup = anchor(panel, trigger, {
    placement: 'bottom-end',
    offset: 8,
  })

  // Later, when the surface closes or unmounts:
  cleanup()
}

Use the returned cleanup function with the lifecycle that owns the floating element. For native popovers, position on open and clean up on close.

import { anchor } from 'remix/ui/anchor'

let cleanupAnchor = () => {}

button.addEventListener('click', () => {
  popover.showPopover()
})

popover.addEventListener('beforetoggle', (event) => {
  let toggleEvent = event as ToggleEvent

  if (toggleEvent.newState === 'open') {
    cleanupAnchor()
    cleanupAnchor = anchor(popover, button, {
      placement: 'bottom-start',
      offset: 4,
    })
    return
  }

  cleanupAnchor()
  cleanupAnchor = () => {}
})

anchor.*

  • anchor(floatingElement, anchorElement, options): positions floatingElement against anchorElement, starts animation-frame polling for geometry changes, and returns a cleanup function.
  • AnchorOptions: placement, inset, relative alignment, and offset options.
  • AnchorPlacement: exported placement names for the main sides and top/bottom start/end alignment.

Placements

Default placement is bottom. Use start/end variants to align an edge instead of centering the floating element on the anchor.

anchor(panel, trigger, { placement: 'bottom' })
anchor(panel, trigger, { placement: 'bottom-start' })
anchor(panel, trigger, { placement: 'bottom-end' })
anchor(panel, trigger, { placement: 'top' })
anchor(panel, trigger, { placement: 'left' })
anchor(panel, trigger, { placement: 'right' })

The positioning logic can also handle left/right start/end placements through AnchorOptions['placement'].

anchor(panel, trigger, { placement: 'right-start' })
anchor(panel, trigger, { placement: 'left-end' })

When the requested placement would overflow the viewport, anchor flips to the opposite side and writes the final placement to data-anchor-placement.

let cleanup = anchor(panel, trigger, {
  placement: 'bottom-start',
})

panel.dataset.anchorPlacement

Offsets

Use offset for distance along the placement axis. Use offsetX and offsetY for independent adjustment after placement is resolved.

anchor(panel, trigger, {
  placement: 'bottom-start',
  offset: 8,
  offsetX: 4,
  offsetY: -2,
})

Offsets may be numbers or functions that receive the floating element.

anchor(panel, trigger, {
  placement: 'bottom-start',
  offset: (floating) => floating.offsetHeight / 10,
  offsetX: (floating) => floating.offsetWidth / 20,
})

Inset Positioning

Pass inset: true to align the floating element inside the anchor edge instead of outside it. This is useful for surfaces that should visually cover or line up with the trigger.

anchor(panel, trigger, {
  placement: 'bottom-start',
  inset: true,
})

Relative Alignment

Use relativeTo when a child inside the floating element should align to the anchor instead of the floating element's outer box. The value is a selector scoped to the floating element.

anchor(listbox, trigger, {
  placement: 'bottom-start',
  relativeTo: '[role="option"][aria-selected="true"]',
})

Combine relativeTo with inset for selected-option popovers where the selected option should sit over the trigger.

anchor(listbox, trigger, {
  placement: 'left',
  inset: true,
  relativeTo: '[aria-selected="true"]',
})

Behavior Notes

  • Default placement is below the anchor.
  • Supported placements include top, bottom, left, right, top/bottom start/end variants, and left/right start/end placements through AnchorOptions['placement'].
  • The floating element flips when the requested placement would overflow the viewport and records the final placement in data-anchor-placement.
  • Oversized floating elements are constrained with max dimensions and remain inside the viewport padding.
  • Oversized inset surfaces with relativeTo preserve alignment by scrolling the nearest scrollable descendant when possible.
  • offset, offsetX, and offsetY may be numbers or functions that receive the floating element.
  • relativeTo lets a surface align to an inner element, which is useful for selected options inside popovers.
  • anchor polls on animation frames for anchor or floating geometry changes and repositions when either changes.
  • The returned cleanup function cancels animation-frame polling.