anchor_kit 0.2.1 copy "anchor_kit: ^0.2.1" to clipboard
anchor_kit: ^0.2.1 copied to clipboard

Headless anchor-positioning primitives for popovers, tooltips, dropdowns and menus. A Floating UI for Flutter.

anchor_kit #

pub package license: MIT

Headless anchor-positioning primitives for Flutter — the reusable engine behind popovers, tooltips, dropdowns, context menus and select inputs. Inspired by Floating UI (formerly Popper.js).

Flutter ships great Tooltip and menu widgets, but there's no reusable engine that composes a positioning strategy from small, testable pieces (offset, flip, shift, arrow). anchor_kit is that missing primitive: a pure computePosition function plus a FloatingOverlay widget, so higher-level UI kits don't have to reinvent placement math.

Status: early but usable. The core is covered by tests and runs on every platform (mobile, desktop, web). Some middleware from Floating UI is not implemented yet — see Known limitations.

This README describes the library as it is now and carries no version numbers — the badge above is the current version, and CHANGELOG.md is the history.

Popover with an arrow, placed above its anchor

What can you build with it? #

It's not popover-only — it positions any floating element relative to an anchor. The example app ships these recipes:

Tooltip Dropdown menu Select (flips up)
tooltip dropdown select
Popover + arrow Placement playground Recipe gallery
popover playground recipes

Run them yourself:

cd example
flutter run              # mobile / desktop
flutter run -d chrome    # web

Install #

flutter pub add anchor_kit

Quick start #

FloatingOverlay(
  isOpen: open,
  placement: Placement.bottomStart,
  middleware: [
    OffsetMiddleware(8),   // gap from the anchor
    Flip(padding: 8),      // flip to the other side if there's no room
    Shift(padding: 8),     // slide back on-screen if it overflows
  ],
  barrierDismissible: true,           // tap outside to close
  onDismiss: () => setState(() => open = false),
  floating: (context, position) => Material(
    elevation: 8,
    child: Padding(
      padding: const EdgeInsets.all(12),
      child: Text('Placed on ${position.placement.side.name}'),
    ),
  ),
  child: ElevatedButton(
    onPressed: () => setState(() => open = !open),
    child: const Text('Open'),
  ),
)

Or use the pure function with no widgets at all — great for tests:

final result = computePosition(
  anchor: Rect.fromLTWH(100, 200, 40, 40),
  floating: const Size(180, 60),
  viewport: Rect.fromLTWH(0, 0, 400, 800),
  placement: Placement.top,
  middleware: [Flip(), Shift()],
);
// result.offset, result.placement, result.middlewareData['flipped']

Concepts #

Placement #

Where the floating element goes relative to the anchor: a side (top/right/bottom/left) × an alignment (start/center/end) = 12 placements (Placement.top, Placement.bottomStart, …).

Middleware #

Small composable rules, run in order, each adjusting the position. Recommended order: OffsetMiddlewareFlipShiftArrow.

  • OffsetMiddleware(distance, {crossAxis}) — pushes the floating element away from the anchor (the gap). crossAxis nudges along the perpendicular axis.

  • Flip({padding}) — if the current side would overflow, flips to the opposite side. If neither side fully fits, keeps the one that overflows least.

  • Shift({padding, mainAxis, crossAxis, rtl}) — slides the element along the viewport so it stays visible, without changing the side. Clamps the main axis (parallel to the anchor edge) by default; crossAxis: true also clamps toward the anchor (can detach it); rtl: true keeps the right edge for oversized elements.

    Without Shift:              With Shift:
    ┌─────────────┐            ┌─────────────┐
    │        [btn]│            │        [btn]│
    │        ┌────┼── ✂        │     ┌──────┐│
    │        │ popover         │     │popover ││ ← slid inward
    └─────────────┘            └─────────────┘
    

    (Flip changes the side; Shift keeps the side and moves along the edge. They're usually used together.)

  • AutoPlacement({padding, candidates}) — chooses the side with the most room (keeps the current side if it fits, else least overflow). Use instead of Flip. Records the pick in data['autoPlacement'].

  • SizeMiddleware({padding}) — reports the space available on the resolved side in data['size'] = {availableWidth, availableHeight}, so a long menu can cap its height to the viewport and scroll internally. See the "Size" example recipe.

  • Hide({padding}) — flags data['hide'] = {referenceHidden} when the anchor scrolls off-screen, so you can hide the floating element.

  • Arrow({padding, arrowSize}) — computes where a little arrow should sit so it points at the anchor's centre. Read it from position.middlewareData['arrow'] ({x?, y?}) — see ArrowBubble in the example for a ready-made bubble.

computePosition returns a PositionResult with the final offset, the resolved placement (after any flip), and a middlewareData map that middleware write into (flipped, shift, arrow).

FloatingOverlay #

Renders floating into the app Overlay while isOpen, and keeps it glued to the anchor:

  • Follows the anchor every frame — ancestor scrolling, window resize/rotation, or the anchor moving all re-position it.
  • Re-measures the floating child, so content that changes size stays placed.
  • Optional barrierDismissible + onDismiss for tap-outside-to-close (dropdowns / selects / popovers).

Performance #

Positioning is a pure synchronous function with no allocations beyond the result. Measured on an Apple M3 Pro (flutter test, JIT — an AOT release build is faster):

Pipeline per call throughput
base placement, no middleware ~0.03 µs ~36 M/sec
full 6-stage chain (offset → flip → shift → size → hide → arrow) ~0.64 µs ~1.5 M/sec

A 60 fps frame gives you a 16.7 ms budget — room for tens of thousands of full-chain positionings. In practice a popover's cost is Flutter's overlay layout, not computePosition.

Run it yourself: the example app ships a Stress test recipe with a live FPS / build / raster / jank readout and a computePosition micro-benchmark (flutter run --profile for representative numbers).

Known limitations #

Honest scope (vs. Floating UI). None are blockers for the use cases above:

  • No virtual/rect reference elements — the anchor is always a widget.
  • FloatingOverlay provides positioning + an optional dismiss barrier, but not focus trapping / keyboard navigation / enter-exit animation — compose those yourself for now.
  • Middleware run in a single pass; SizeMiddleware reports available space and the floating element settles to fit over one extra frame (fine in practice, as FloatingOverlay re-measures every frame).

See doc/ROADMAP.md and doc/SPEC.md.

License #

MIT

2
likes
150
points
284
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Headless anchor-positioning primitives for popovers, tooltips, dropdowns and menus. A Floating UI for Flutter.

Repository (GitHub)
View/report issues

Topics

#ui #positioning #popover #tooltip #overlay

License

MIT (license)

Dependencies

flutter

More

Packages that depend on anchor_kit