Overlay

Popover

A popover displays rich content in a portal that is aligned to a child.

FPopover(
  popoverAnchor: Alignment.topCenter,
  childAnchor: Alignment.bottomCenter,
  popoverBuilder: (context, _) => Padding(
    padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
    child: SizedBox(
      width: 288,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Dimensions', style: context.theme.typography.base),
          const SizedBox(height: 7),
          Text(
            'Set the dimensions for the layer.',
            style: context.theme.typography.sm.copyWith(
              color: context.theme.colors.mutedForeground,
              fontWeight: FontWeight.w300,
            ),
          ),
          const SizedBox(height: 15),
          for (final (index, (label, value)) in [
            ('Width', '100%'),
            ('Max. Width', '300px'),
            ('Height', '25px'),
            ('Max. Height', 'none'),
          ].indexed) ...[
            Row(
              children: [
                Expanded(child: Text(label, style: context.theme.typography.sm)),
                Expanded(
                  flex: 2,
                  child: FTextField(
                    control: FTextFieldControl.managed(initial: TextEditingValue(text: value)),
                    autofocus: index == 0,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 7),
          ],
        ],
      ),
    ),
  ),
  builder: (_, controller, _) => FButton(
    style: FButtonStyle.outline(),
    mainAxisSize: MainAxisSize.min,
    onPress: controller.toggle,
    child: const Text('Open popover'),
  ),
);

CLI

To generate and customize this style:

dart run forui style create popover

Usage

FPopover(...)

FPopover(
  control: FPopoverControl.managed(),
  style: (style) => style.copyWith(...),
  constraints: const FPortalConstraints(),
  popoverAnchor: Alignment.topCenter,
  childAnchor: Alignment.bottomCenter,
  spacing: const FPortalSpacing(4),
  overflow: FPortalOverflow.flip,
  offset: Offset.zero,
  groupId: 'popover-group',
  hideRegion: FPopoverHideRegion.excludeChild,
  onTapHide: () {},
  popoverBuilder: (context, controller) => const Placeholder(),
  builder: (context, controller, child) => const Placeholder(),
  child: const Placeholder(),
);

Examples

Nested Popover

When placing widgets that use popovers internally, e.g. FSelect inside an FPopover, the outer popover will close when interacting with the inner widget's dropdown. This happens because the inner dropdown is rendered in a separate overlay layer, and tapping it is considered "outside" the outer popover.

To prevent this, make both widgets share the same groupId.

FPopover(
  groupId: 'nested-popover',
  popoverBuilder: (context, _) => Padding(
    padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
    child: SizedBox(
      width: 288,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Dimensions', style: context.theme.typography.base),
          const SizedBox(height: 7),
          Text(
            'Set the dimensions for the layer.',
            style: context.theme.typography.sm.copyWith(
              color: context.theme.colors.mutedForeground,
              fontWeight: FontWeight.w300,
            ),
          ),
          const SizedBox(height: 15),
          Row(
            children: [
              Expanded(child: Text('Width', style: context.theme.typography.sm)),
              Expanded(
                flex: 2,
                child: FSelect<String>.rich(
                  contentGroupId: 'nested-popover',
                  hint: 'Select',
                  format: (s) => s,
                  children: [
                    FSelectItem.item(title: const Text('100%'), value: '100%'),
                    FSelectItem.item(title: const Text('75%'), value: '75%'),
                    FSelectItem.item(title: const Text('50%'), value: '50%'),
                  ],
                ),
              ),
            ],
          ),
        ],
      ),
    ),
  ),
  builder: (_, controller, _) => FButton(
    style: FButtonStyle.outline(),
    mainAxisSize: MainAxisSize.min,
    onPress: controller.toggle,
    child: const Text('Open popover'),
  ),
);

Horizontal Alignment

You can change how the popover is aligned to the button.

FPopover(
  popoverAnchor: Alignment.bottomLeft,
  childAnchor: Alignment.bottomRight,
  popoverBuilder: (context, _) => Padding(
    padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
    child: SizedBox(
      width: 288,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Dimensions', style: context.theme.typography.base),
          const SizedBox(height: 7),
          Text(
            'Set the dimensions for the layer.',
            style: context.theme.typography.sm.copyWith(
              color: context.theme.colors.mutedForeground,
              fontWeight: FontWeight.w300,
            ),
          ),
          const SizedBox(height: 15),
          for (final (index, (label, value)) in [
            ('Width', '100%'),
            ('Max. Width', '300px'),
            ('Height', '25px'),
            ('Max. Height', 'none'),
          ].indexed) ...[
            Row(
              children: [
                Expanded(child: Text(label, style: context.theme.typography.sm)),
                Expanded(
                  flex: 2,
                  child: FTextField(
                    control: FTextFieldControl.managed(initial: TextEditingValue(text: value)),
                    autofocus: index == 0,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 7),
          ],
        ],
      ),
    ),
  ),
  builder: (_, controller, _) => FButton(
    style: FButtonStyle.outline(),
    mainAxisSize: MainAxisSize.min,
    onPress: controller.toggle,
    child: const Text('Open popover'),
  ),
);

Tapping Outside Does Not Close Popover

FPopover(
  hideRegion: FPopoverHideRegion.none,
  popoverBuilder: (context, _) => Padding(
    padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
    child: SizedBox(
      width: 288,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Dimensions', style: context.theme.typography.base),
          const SizedBox(height: 7),
          Text(
            'Set the dimensions for the layer.',
            style: context.theme.typography.sm.copyWith(
              color: context.theme.colors.mutedForeground,
              fontWeight: FontWeight.w300,
            ),
          ),
          const SizedBox(height: 15),
          for (final (index, (label, value)) in [
            ('Width', '100%'),
            ('Max. Width', '300px'),
            ('Height', '25px'),
            ('Max. Height', 'none'),
          ].indexed) ...[
            Row(
              children: [
                Expanded(child: Text(label, style: context.theme.typography.sm)),
                Expanded(
                  flex: 2,
                  child: FTextField(
                    control: FTextFieldControl.managed(initial: TextEditingValue(text: value)),
                    autofocus: index == 0,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 7),
          ],
        ],
      ),
    ),
  ),
  builder: (_, controller, _) => FButton(
    style: FButtonStyle.outline(),
    mainAxisSize: MainAxisSize.min,
    onPress: controller.toggle,
    child: const Text('Open popover'),
  ),
);

Blurred Barrier

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.end,
  children: [
    Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Layer Properties', style: context.theme.typography.xl.copyWith(fontWeight: FontWeight.bold)),
        const SizedBox(height: 20),
        const FTextField(control: FTextFieldControl.managed(initial: TextEditingValue(text: 'Header Component'))),
        const SizedBox(height: 16),
        const FTextField(control: FTextFieldControl.managed(initial: TextEditingValue(text: 'Navigation Bar'))),
        const SizedBox(height: 30),
      ],
    ),
    FPopover(
      style: (style) => style.copyWith(
        barrierFilter: (animation) => ImageFilter.compose(
          outer: ImageFilter.blur(sigmaX: animation * 5, sigmaY: animation * 5),
          inner: ColorFilter.mode(
            Color.lerp(Colors.transparent, Colors.black.withValues(alpha: 0.2), animation)!,
            BlendMode.srcOver,
          ),
        ),
      ),
      popoverAnchor: Alignment.topCenter,
      childAnchor: Alignment.bottomCenter,
      popoverBuilder: (context, _) => Padding(
        padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
        child: SizedBox(
          width: 288,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Dimensions', style: context.theme.typography.base),
              const SizedBox(height: 7),
              Text(
                'Set the dimensions for the layer.',
                style: context.theme.typography.sm.copyWith(
                  color: context.theme.colors.mutedForeground,
                  fontWeight: FontWeight.w300,
                ),
              ),
              const SizedBox(height: 15),
              for (final (index, (label, value)) in [
                ('Width', '100%'),
                ('Max. Width', '300px'),
                ('Height', '25px'),
                ('Max. Height', 'none'),
              ].indexed) ...[
                Row(
                  children: [
                    Expanded(child: Text(label, style: context.theme.typography.sm)),
                    Expanded(
                      flex: 2,
                      child: FTextField(
                        control: FTextFieldControl.managed(initial: TextEditingValue(text: value)),
                        autofocus: index == 0,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 7),
              ],
            ],
          ),
        ),
      ),
      builder: (_, controller, _) => FButton(
        style: FButtonStyle.outline(),
        mainAxisSize: MainAxisSize.min,
        onPress: controller.toggle,
        child: const Text('Open popover'),
      ),
    ),
  ],
);

Flip along Axis

The popover can be flipped along the overflowing axis to stay within the viewport boundaries.

FPopover(
  overflow: FPortalOverflow.flip,
  popoverBuilder: (context, _) => Padding(
    padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
    child: SizedBox(
      width: 288,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Dimensions', style: context.theme.typography.base),
          const SizedBox(height: 7),
          Text(
            'Set the dimensions for the layer.',
            style: context.theme.typography.sm.copyWith(
              color: context.theme.colors.mutedForeground,
              fontWeight: FontWeight.w300,
            ),
          ),
          const SizedBox(height: 15),
          for (final (index, (label, value)) in [
            ('Width', '100%'),
            ('Max. Width', '300px'),
            ('Height', '25px'),
            ('Max. Height', 'none'),
          ].indexed) ...[
            Row(
              children: [
                Expanded(child: Text(label, style: context.theme.typography.sm)),
                Expanded(
                  flex: 2,
                  child: FTextField(
                    control: FTextFieldControl.managed(initial: TextEditingValue(text: value)),
                    autofocus: index == 0,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 7),
          ],
        ],
      ),
    ),
  ),
  builder: (_, controller, _) => FButton(
    style: FButtonStyle.outline(),
    mainAxisSize: MainAxisSize.min,
    onPress: controller.toggle,
    child: const Text('Open popover'),
  ),
);

Slide along Axis

The popover can be slid along the overflowing axis to stay within the viewport boundaries.

FPopover(
  overflow: FPortalOverflow.slide,
  popoverBuilder: (context, _) => Padding(
    padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
    child: SizedBox(
      width: 288,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Dimensions', style: context.theme.typography.base),
          const SizedBox(height: 7),
          Text(
            'Set the dimensions for the layer.',
            style: context.theme.typography.sm.copyWith(
              color: context.theme.colors.mutedForeground,
              fontWeight: FontWeight.w300,
            ),
          ),
          const SizedBox(height: 15),
          for (final (index, (label, value)) in [
            ('Width', '100%'),
            ('Max. Width', '300px'),
            ('Height', '25px'),
            ('Max. Height', 'none'),
          ].indexed) ...[
            Row(
              children: [
                Expanded(child: Text(label, style: context.theme.typography.sm)),
                Expanded(
                  flex: 2,
                  child: FTextField(
                    control: FTextFieldControl.managed(initial: TextEditingValue(text: value)),
                    autofocus: index == 0,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 7),
          ],
        ],
      ),
    ),
  ),
  builder: (_, controller, _) => FButton(
    style: FButtonStyle.outline(),
    mainAxisSize: MainAxisSize.min,
    onPress: controller.toggle,
    child: const Text('Open popover'),
  ),
);

Allow Overflow

The popover is not shifted to stay within the viewport boundaries, even if it overflows.

FPopover(
  overflow: FPortalOverflow.none,
  popoverBuilder: (context, _) => Padding(
    padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
    child: SizedBox(
      width: 288,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Dimensions', style: context.theme.typography.base),
          const SizedBox(height: 7),
          Text(
            'Set the dimensions for the layer.',
            style: context.theme.typography.sm.copyWith(
              color: context.theme.colors.mutedForeground,
              fontWeight: FontWeight.w300,
            ),
          ),
          const SizedBox(height: 15),
          for (final (index, (label, value)) in [
            ('Width', '100%'),
            ('Max. Width', '300px'),
            ('Height', '25px'),
            ('Max. Height', 'none'),
          ].indexed) ...[
            Row(
              children: [
                Expanded(child: Text(label, style: context.theme.typography.sm)),
                Expanded(
                  flex: 2,
                  child: FTextField(
                    control: FTextFieldControl.managed(initial: TextEditingValue(text: value)),
                    autofocus: index == 0,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 7),
          ],
        ],
      ),
    ),
  ),
  builder: (_, controller, _) => FButton(
    style: FButtonStyle.outline(),
    mainAxisSize: MainAxisSize.min,
    onPress: controller.toggle,
    child: const Text('Open popover'),
  ),
);

On this page