Skip to content

Placement API

API Reference: Engine · Types · Storage · Events · Security · Servers · This Page · Lightbox · Lens · Embeds · Coordinators · Config Registry

Pure-geometry placement engine. Positions menus, overlays, and panels relative to trigger elements without touching the DOM.

See also: Cookbook: Placement for usage examples.

Which import?

typescript
// Browser — use with AlapUI or web components
import { computePlacement, parsePlacement } from 'alap';

// No DOM — use in tests or build tools
import { computePlacement, parsePlacement } from 'alap/core';

computePlacement(input)

Computes where to position a menu relative to a trigger element. Pure geometry — no DOM access. Takes viewport-coordinate rects, returns viewport-coordinate results.

typescript
const result = computePlacement({
  triggerRect: trigger.getBoundingClientRect(),
  menuSize: { width: menu.offsetWidth, height: menu.offsetHeight },
  viewport: { width: innerWidth, height: innerHeight },
  placement: 'SE',
  strategy: 'flip',
});

// result.x, result.y — top-left of menu in viewport coords
// result.placement — actual placement used (may differ if flipped)
// result.scrollY — true if menu content should scroll

PlacementInput

FieldTypeDefaultDescription
triggerRectRectrequiredTrigger element rect from getBoundingClientRect()
menuSizeSizerequiredNatural (unconstrained) menu dimensions
viewportSizerequired{ width: innerWidth, height: innerHeight }
placementPlacement'SE'Preferred compass direction
strategyPlacementStrategy'flip'How hard to try to fit
gapnumber4Pixel gap between trigger and menu edge
paddingnumber8Minimum distance from viewport edges

PlacementResult

FieldTypeDescription
placementPlacementThe placement actually used (may differ from requested)
xnumberMenu top-left x in viewport coordinates
ynumberMenu top-left y in viewport coordinates
maxWidthnumber?Clamped max width (only set if menu needed to shrink)
maxHeightnumber?Clamped max height (only set if menu needed to shrink)
scrollYbooleanWhether menu content should scroll vertically

parsePlacement(input)

Parses a comma-separated placement string into a compass direction and strategy.

typescript
parsePlacement('SE')          // → { compass: 'SE', strategy: 'flip' }
parsePlacement('N, clamp')    // → { compass: 'N', strategy: 'clamp' }
parsePlacement('clamp')       // → { compass: 'SE', strategy: 'clamp' }
parsePlacement('SW, place')   // → { compass: 'SW', strategy: 'place' }

Unknown tokens are silently discarded. If multiple compass directions appear, uses the first. If multiple strategies appear, uses the highest effort.

Types

Placement

Nine compass directions plus center:

typescript
type Placement = 'N' | 'NE' | 'E' | 'SE' | 'S' | 'SW' | 'W' | 'NW' | 'C';

PlacementStrategy

Three levels of effort, each building on the last:

StrategyBehavior
'place'Position at compass point. No fallback, no clamping.
'flip'Place + try fallbacks if it doesn't fit. No clamping. (default)
'clamp'Flip + constrain to viewport + scroll long menus.

Rect

typescript
interface Rect {
  top: number;
  left: number;
  bottom: number;
  right: number;
  width: number;
  height: number;
}

Same shape as DOMRect.

Size

typescript
interface Size { width: number; height: number; }

FALLBACK_ORDER

When the flip or clamp strategy can't fit at the requested position, it walks this fallback order. Tries the opposite direction first, then adjacent, then remaining:

Show fallback table
typescript
const FALLBACK_ORDER: Record<Placement, Placement[]> = {
  N:  ['S',  'NE', 'NW', 'SE', 'SW', 'E',  'W',  'C'],
  NE: ['SW', 'SE', 'NW', 'S',  'N',  'E',  'W',  'C'],
  E:  ['W',  'SE', 'NE', 'SW', 'NW', 'S',  'N',  'C'],
  SE: ['NW', 'NE', 'SW', 'S',  'N',  'E',  'W',  'C'],
  S:  ['N',  'SE', 'SW', 'NE', 'NW', 'E',  'W',  'C'],
  SW: ['NE', 'NW', 'SE', 'S',  'N',  'W',  'E',  'C'],
  W:  ['E',  'NW', 'SW', 'NE', 'SE', 'N',  'S',  'C'],
  NW: ['SE', 'SW', 'NE', 'N',  'S',  'W',  'E',  'C'],
  C:  ['SE', 'NE', 'SW', 'NW', 'S',  'N',  'E',  'W'],
};

DOM helpers

These functions bridge computePlacement to the actual DOM. Used internally by all adapters.

calcPlacementState(triggerEl, menuEl, options)

Measures trigger and menu, calls computePlacement, returns the result. Framework-agnostic — each adapter calls this in its own reactive context.

applyPlacementToMenu(menuEl, wrapperEl, state)

Applies a PlacementResult to a menu element using wrapper-relative offsets. The menu must be inside a wrapper with position: relative.

applyPlacementAfterLayout(triggerEl, menuEl, wrapperEl, options)

Schedules placement after the browser has completed layout via requestAnimationFrame. Returns a synchronous apply function for scroll handlers where rAF latency is unwanted.

calcPlacementAfterLayout(triggerEl, menuEl, options, onPlacement)

Same as above but for adapters that position via viewport coordinates (e.g. Alpine, which appends menus to document.body). Calls the provided callback with the result.

applyPlacementClass(el, placement) / clearPlacementClass(el)

Adds or removes alap-placed-{direction} CSS classes (e.g. alap-placed-se). Validates against the known set to prevent injection.

observeTriggerOffscreen(triggerEl, onOffscreen)

Returns an IntersectionObserver that fires when the trigger scrolls fully off-screen. Disconnect it on menu close.