Skip to content

Popover with Arrow

Problem

Implement popover with arrow in a production-friendly way with @vielzeug/floatit while keeping setup and cleanup explicit.

Runnable Example

The snippet below is copy-paste runnable in a TypeScript project with @vielzeug/floatit installed.

Align an arrow element with the placement side by using dataset.placement.

ts
import { autoUpdate, flip, offset, positionFloat, shift } from '@vielzeug/floatit';

const trigger = document.querySelector<HTMLElement>('#btn')!;
const popover = document.querySelector<HTMLElement>('#popover')!;

let cleanup: (() => void) | null = null;

function update() {
  popover.dataset.placement = positionFloat(trigger, popover, {
    placement: 'top',
    middleware: [offset(12), flip(), shift({ padding: 8 })],
  });
}

trigger.addEventListener('click', () => {
  if (popover.hasAttribute('data-open')) {
    popover.removeAttribute('data-open');
    cleanup?.();
    cleanup = null;
  } else {
    popover.setAttribute('data-open', '');
    cleanup = autoUpdate(trigger, popover, update);
  }
});
css
/* Arrow pointing down (placement = top) */
#popover[data-placement='top'] .arrow {
  bottom: -5px;
}
#popover[data-placement='bottom'] .arrow {
  top: -5px;
}
#popover[data-placement='left'] .arrow {
  right: -5px;
}
#popover[data-placement='right'] .arrow {
  left: -5px;
}

Expected Output

  • The example runs without type errors in a standard TypeScript setup.
  • The main flow produces the behavior described in the recipe title.

Common Pitfalls

  • Forgetting cleanup/dispose calls can leak listeners or stale state.
  • Skipping explicit typing can hide integration issues until runtime.
  • Not handling error branches makes examples harder to adapt safely.