---
title: NakedPopover
description: Headless anchored popover with focus management, outside-tap dismissal, and lifecycle hooks
keywords: [flutter, popover, overlay, anchored, headless, focus]
---

Headless popover component. Handles positioning, focus management, outside dismissal, and lifecycle. Use builder pattern for custom styling.

## When to use this

- **Additional information**: Show details or help text on demand
- **Form fields**: Display validation errors or input help
- **Action confirmations**: "Are you sure?" confirmations for buttons
- **Content previews**: Preview links, images, or documents

<Info>
  Explore the sample in [`packages/example/lib/api/naked_popover.0.dart`](https://github.com/btwld/naked_ui/blob/main/packages/example/lib/api/naked_popover.0.dart).
</Info>

## Basic implementation

```dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

class PopoverExample extends StatelessWidget {
  const PopoverExample({super.key});

  @override
  Widget build(BuildContext context) {
    return NakedPopover(
      positioning: const OverlayPositionConfig(
        targetAnchor: Alignment.bottomCenter,
        followerAnchor: Alignment.topCenter,
      ),
      popoverBuilder: (context, info) {
        return Material(
          elevation: 6,
          borderRadius: BorderRadius.circular(12),
          child: SizedBox(
            width: 220,
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('Invite teammates',
                      style: TextStyle(fontWeight: FontWeight.w600)),
                  const SizedBox(height: 8),
                  const Text('Share this project link to collaborate.'),
                  const SizedBox(height: 12),
                  ElevatedButton(
                    onPressed: () => Navigator.of(context).maybePop(),
                    child: const Text('Copy link'),
                  ),
                ],
              ),
            ),
          ),
        );
      },
      builder: (context, state, _) {
        final bool pressed = state.isPressed;
        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
          decoration: BoxDecoration(
            color: pressed ? Colors.blue.shade700 : Colors.blue.shade600,
            borderRadius: BorderRadius.circular(8),
          ),
          child: const Text('Open popover', style: TextStyle(color: Colors.white)),
        );
      },
    );
  }
}
```

## Animated Popover with Lifecycle Hooks

```dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

class AnimatedPopover extends StatefulWidget {
  const AnimatedPopover({super.key});

  @override
  State<AnimatedPopover> createState() => _AnimatedPopoverState();
}

class _AnimatedPopoverState extends State<AnimatedPopover>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    duration: const Duration(milliseconds: 200),
    vsync: this,
  );

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NakedPopover(
      positioning: const OverlayPositionConfig(),
      onOpen: () => _controller.forward(),
      onClose: () => _controller.reverse(),
      popoverBuilder: (context, info) {
        return FadeTransition(
          opacity: _controller,
          child: ScaleTransition(
            scale: Tween<double>(begin: 0.9, end: 1.0).animate(
              CurvedAnimation(parent: _controller, curve: Curves.easeOutBack),
            ),
            child: DecoratedBox(
              decoration: BoxDecoration(
                color: Colors.black,
                borderRadius: BorderRadius.circular(12),
              ),
              child: const Padding(
                padding: EdgeInsets.all(16),
                child: const Text(
                  'Animated popover content',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
        );
      },
      child: const Icon(Icons.info_outline, color: Colors.black87),
    );
  }
}
```

## Constructor

```dart
const NakedPopover({
  Key? key,
  this.child,
  this.builder,
  required this.popoverBuilder,
  this.positioning = const OverlayPositionConfig(),
  this.consumeOutsideTaps = true,
  this.useRootOverlay = false,
  this.openOnTap = true,
  this.triggerFocusNode,
  this.onOpen,
  this.onClose,
  this.onOpenRequested,
  this.onCloseRequested,
  this.controller,
  this.semanticLabel,
  this.excludeSemantics = false,
})
```

### Key Parameters

- `child` / `builder` → provide a static trigger or a `ValueWidgetBuilder<NakedPopoverState>` for dynamic styling
- `popoverBuilder` → `(BuildContext, RawMenuAnchorOverlayInfo)` to build the overlay content
- `positioning` → `OverlayPositionConfig` controlling alignment/offset/fallbacks
- `openOnTap` → disable to manage open state manually (e.g. hover triggers)
- `consumeOutsideTaps` / `useRootOverlay` → control gesture capture and overlay target
- `triggerFocusNode` → provide an external `FocusNode` for restoring focus after close
- Lifecycle hooks: `onOpen`, `onClose`, `onOpenRequested`, `onCloseRequested`
- `semanticLabel` → provide an accessible name for the popover trigger
- `excludeSemantics` → hide the popover trigger subtree from the semantic tree; opened popover content remains exposed

## Behaviour & Accessibility

- Tap trigger or press Space/Enter to open when `openOnTap` is true
- Escape closes and returns focus to the trigger
- Outside taps close automatically (configurable via `consumeOutsideTaps`)
- Wrap complex content in `FocusTraversalGroup` or provide your own focusable widgets inside the popover for keyboard navigation
- Provide a `semanticLabel` inside the popover content as needed (e.g. heading)

## Positioning Helpers

Use `OverlayPositionConfig` to adjust anchor placement:

```dart
const OverlayPositionConfig(
  targetAnchor: Alignment.bottomCenter,
  followerAnchor: Alignment.topCenter,
  offset: Offset(0, 8),
);
```

The builder receives `RawMenuAnchorOverlayInfo` (`info` in the sample) with `anchorRect`, `overlaySize`, and `position` should you need custom math.
