Creating Custom Dropdowns with OverlayPortal in Flutter

Payam Zahedi
Snapp X
Published in
6 min readAug 22, 2023

Problem Statement

One of the most common widgets in Flutter is the Material Dropdown. It proves to be highly useful when working with forms. However, there is a problem: It’s not customizable! How could you create something like the image below? Achieving this is impossible with the current Material Dropdown in Flutter.

In this article, we will learn how to create our own custom Dropdown using OverlayPortal. OverlayPortal is a new Flutter widget introduced after Flutter version 3.10. This new API empowers us to create overlays in a declarative way.

So, here’s the plan: we’ll take you on a guided tour of OverlayPortal — exploring what it is, how to use it, and the exciting possibilities it opens up for creative designs. Additionally, we’ll walk you through the process of building our custom dropdown utilizing the capabilities of OverlayPortal.

The Goal: A Custom Dropdown

After we learned about OverlayPortal our subsequent goal is to create the following custom drop down widget.

Introducing the flex_dropdown Package!

New Addition: Based on the concepts covered in this article, we’ve developed a convenient Flutter package for custom dropdowns. now you can easily integrate dropdowns using our Custom Dropdown Package.

https://github.com/Snapp-X/flex_dropdown

Understanding Overlay and OverlayPortal

The question is: What exactly is Overlayand OverlayPortal?

Before diving into creating our custom dropdown, let’s understand the basics of Overlay and the capabilities OverlayPortal brings to the table.

Overlay

As outlined in the documentation:

Overlays let independent child widgets “float” visual elements on top of other widgets by inserting them into the overlay’s stack. The overlay lets each of these widgets manage their participation in the overlay using OverlayEntry objects.

Utilizing Overlay enables you to display certain widgets on top of others that have been presented through the navigator.

However, it’s important to note that the overlay API functions in an imperative manner. This mean you should take care of the entire lifecycle of your widget, like creation, presentation, and concealment. Moreover, when you are using Overlay, it seems that you don’t have access to the InheritedWidgets like Theme or other useful widgets.

OverlayPortal

OverlayPortal in the other hand is Declarative, and it has access to Inherited widget.

Documentation:

The OverlayPortal uses overlayChildBuilder to build its overlay child and renders it on the specified Overlay as if it was inserted using an OverlayEntry, while it can depend on the same set of InheritedWidgets (such as Theme) that this widget can depend on.

Custom Drop Down

Flutter’s Material Dropdown serves as a handy Widget, But its customization capabilities have their boundaries. For instance, envision wanting to create a grid-based dropdown menu, you can not achieve it by using material drop down in Flutter.

Let’s jump into the code. The image below displays our final dropdown design, which includes a button and a menu. To keep things straightforward, we’ve kept the button and menu as simple as can be.

Starting Simple

We’ll begin with a basic example and gradually enhance it. In the initial step, the Button widget is a basic Material Widget that responds to taps, while the Menu widget is a simple container with a gray color.

class ButtonWidget extends StatelessWidget {
const ButtonWidget({
super.key,
this.height = 48,
this.width,
this.onTap,
this.child,
});

final double? height;
final double? width;

final VoidCallback? onTap;

final Widget? child;

@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
width: width,
child: Material(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.black12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Center(
child: child ?? const SizedBox(),
),
),
),
);
}
}

class MenuWidget extends StatelessWidget {
const MenuWidget({
super.key,
this.width,
});

final double? width;

@override
Widget build(BuildContext context) {
return Container(
width: width ?? 200,
height: 300,
decoration: ShapeDecoration(
color: Colors.black26,
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.5,
color: Colors.black26,
),
borderRadius: BorderRadius.circular(12),
),
shadows: const [
BoxShadow(
color: Color(0x11000000),
blurRadius: 32,
offset: Offset(0, 20),
spreadRadius: -8,
),
],
),
);
}
}

Crafting Our Own DropDown

Let’s get started on making our own dropdown widget. We’ll use OverlayPortal to display the menu on top of the button widget. To control when the menu is shown or hidden, we’ll rely on OverlayPortalController. This class gives us the ability to manage our overlay effectively.

Here’s the starting point of our Custom Dropdown’s code:

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

@override
State<StatefulWidget> createState() => CustomDropDownState();
}

class CustomDropDownState extends State<CustomDropDown> {
final OverlayPortalController _tooltipController = OverlayPortalController();

@override
Widget build(BuildContext context) {
return OverlayPortal(
controller: _tooltipController,
overlayChildBuilder: (BuildContext context) {
return const Positioned(
bottom: 30,
left: 30,
child: MenuWidget(),
);
},
child: ButtonWidget(
onTap: onTap,
child: const Text('Button Text'),
),
);
}

void onTap() {
_tooltipController.toggle();
}
}

and this is the result:

You might have observed that we’re using Positioned to arrange our widget. Another option is to utilize the Align widget instead of Positioned.

@override
Widget build(BuildContext context) {
return OverlayPortal(
controller: _tooltipController,
overlayChildBuilder: (BuildContext context) {
return const Align(
alignment: Alignment.bottomCenter,
child: MenuWidget(),
);
},
child: ButtonWidget(
onTap: onTap,
child: const Text('Button Text'),
),
);
}

Result:

Perfect Positioning

To ensure that our menu nestles beneath the button, we’ll use two remarkable Flutter widgets: CompositedTransformTarget and CompositedTransformFollower.

Let’s back to the code. We will add CompositedTransformTarget and CompositedTransformFollower to our code like this:

class CustomDropDownState extends State<CustomDropDown> {
final OverlayPortalController _tooltipController = OverlayPortalController();

final _link = LayerLink();

@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _link,
child: OverlayPortal(
controller: _tooltipController,
overlayChildBuilder: (BuildContext context) {
return CompositedTransformFollower(
link: _link,
targetAnchor: Alignment.bottomLeft,
child: const Align(
alignment: AlignmentDirectional.topStart,
child: MenuWidget(),
),
);
},
child: ButtonWidget(
onTap: onTap,
child: const Text('Button Text'),
),
),
);
}

void onTap() {
_tooltipController.toggle();
}
}

First, in our state class, we create a LayerLink object. Next, we wrap our Button with CompositedTransformTarget and our Menu with CompositedTransformFollower. We then assign the LayerLink to both of these widgets to link them to each other.

And this is the result:

Adapting to Button Dimensions

Now, we’re ready to determine the menu size (width). As you might be aware, we can find RenderObject of our widget and use the size parameter of it.

An important point to note is that you shouldn’t perform this task within the build method. This is because the RenderObject is not yet created, and attempting to do so may result in an exception. We get the size of the widget in the on Tap method.

class CustomDropDownState extends State<CustomDropDown> {
final OverlayPortalController _tooltipController = OverlayPortalController();

final _link = LayerLink();

/// width of the button after the widget rendered
double? _buttonWidth;

@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _link,
child: OverlayPortal(
controller: _tooltipController,
overlayChildBuilder: (BuildContext context) {
return CompositedTransformFollower(
link: _link,
targetAnchor: Alignment.bottomLeft,
child: Align(
alignment: AlignmentDirectional.topStart,
child: MenuWidget(width: _buttonWidth),
),
);
},
child: ButtonWidget(
onTap: onTap,
child: const Text('Button Text'),
),
),
);
}

void onTap() {
_buttonWidth = context.size?.width;
_tooltipController.toggle();
}
}

And this is the Result:

Conclusion

We’ve delved into the concept of overlays and gained a solid understanding of their functioning. We’ve also explored the key elements — LayerLink, CompositedTransformTarget, and CompositedTransformFollower — and successfully crafted our very own dropdown.

Now you have the core logic of your drop-down, and you can customize it as you need.

In this article, we have recreated the dropdown widget (Toast Style), which is a component sourced from the Toastification package, designed by Sepide Moqadasi. To delve into the comprehensive code, simply follow this Link.

Also, if you want to know more about Flutter, you can follow Me and SnappX on Twitter.

--

--

Payam Zahedi
Snapp X

I’m a Software Engineer, Flutter Developer, Speaker & Open Source Lover. find me in PayamZahedi.com