NakedPopover
Headless anchored popover with focus management, outside-tap dismissal, and lifecycle hooks
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
Explore the sample in example/lib/api/naked_popover.0.dart
.
Basic implementation
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(
alignment: Alignment.bottomCenter,
fallbackAlignment: 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
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
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,
})
Key Parameters
child
/builder
→ provide a static trigger or aValueWidgetBuilder<NakedPopoverState>
for dynamic stylingpopoverBuilder
→(BuildContext, RawMenuAnchorOverlayInfo)
to build the overlay contentpositioning
→OverlayPositionConfig
controlling alignment/offset/fallbacksopenOnTap
→ disable to manage open state manually (e.g. hover triggers)consumeOutsideTaps
/useRootOverlay
→ control gesture capture and overlay targettriggerFocusNode
→ provide an externalFocusNode
for restoring focus after close- Lifecycle hooks:
onOpen
,onClose
,onOpenRequested
,onCloseRequested
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:
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.