anchor_kit 0.1.0 copy "anchor_kit: ^0.1.0" to clipboard
anchor_kit: ^0.1.0 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: 0.1.0 — 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.

[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 #

dependencies:
  anchor_kit: ^0.1.0

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}) — slides the element along the viewport so it stays visible, without changing the side:

    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.)

  • 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).

Known limitations #

Honest scope for 0.1.x (vs. Floating UI). None are blockers for the use cases above, but know what's missing:

  • No size middleware (constrain the floating element to the available space).
  • No hide middleware (detach when the anchor scrolls off-screen).
  • No autoPlacement (pick the best of several placements automatically).
  • No virtual/rect reference elements — the anchor is always a widget.
  • Shift clamps on both axes; Floating UI defaults to the main axis only.
  • FloatingOverlay provides positioning + an optional dismiss barrier, but not focus trapping / keyboard navigation / enter-exit animation — compose those yourself for now.

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

License #

MIT

2
likes
0
points
183
downloads

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

unknown (license)

Dependencies

flutter

More

Packages that depend on anchor_kit