popup_dropdown
A highly customizable popup dropdown widget for Flutter with form validation, animated icon rotation, error states, and zero external dependencies.
Features
โจ Animated icon โ suffix icon rotates when the menu opens
๐จ Highly customizable โ 35+ parameters
โ
Form validation โ plugs directly into Flutter's Form
๐ด Error states โ styled border + error text
๐ Disabled state โ opacity + no interaction
๐ท๏ธ Leading icons โ custom widget per item
๐งฉ Custom trailing โ badges, chips, colours per item
๐ช Custom child โ fully replace the trigger
๐ Theme-aware โ respects Material 3 theme
โก Zero dependencies โ only Flutter
Installation
dependencies:
popup_dropdown: ^1.0.0
flutter pub get
Quick Start
import 'package:popup_dropdown/popup_dropdown.dart';
String? _selected;
PopupDropdown<String>(
items: ['Apple', 'Banana', 'Cherry'],
labelExtractor: (item) => item,
hint: 'Pick a fruit',
title: 'Favourite Fruit',
selectedItem: _selected,
onSelected: (v) => setState(() => _selected = v),
)
Usage Examples
1 ยท Basic โ string list
String? _fruit;
PopupDropdown<String>(
items: ['Apple', 'Banana', 'Cherry'],
labelExtractor: (item) => item,
hint: 'Pick a fruit',
selectedItem: _fruit,
onSelected: (v) => setState(() => _fruit = v),
)
2 ยท Object list
class Country {
final String name;
final String flag;
const Country(this.name, this.flag);
}
Country? _country;
PopupDropdown<Country>(
items: countries,
labelExtractor: (c) => '${c.flag} ${c.name}',
hint: 'Select country',
title: 'Country',
selectedItem: _country,
onSelected: (v) => setState(() => _country = v),
)
3 ยท Leading icon per item
class Category {
final String name;
final IconData icon;
const Category(this.name, this.icon);
}
PopupDropdown<Category>(
items: categories,
labelExtractor: (c) => c.name,
title: 'Category',
isRequired: true,
hint: 'Select category',
selectedItem: _category,
onSelected: (v) => setState(() => _category = v),
itemLeadingBuilder: (c) => Icon(c.icon, size: 20),
)
4 ยท Custom trailing per item (badges / chips)
PopupDropdown<String>(
items: ['Low', 'Medium', 'High', 'Critical'],
labelExtractor: (p) => p,
hint: 'Select priority',
title: 'Priority',
selectedItem: _priority,
onSelected: (v) => setState(() => _priority = v),
itemTrailingBuilder: (item, isSelected) => Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: priorityColor(item).withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Text(
item,
style: TextStyle(color: priorityColor(item), fontSize: 11),
),
),
)
5 ยท Form validation
final _formKey = GlobalKey<FormState>();
String? _value;
Form(
key: _formKey,
child: Column(
children: [
PopupDropdown<String>(
items: options,
labelExtractor: (i) => i,
hint: 'Required field',
title: 'Option',
isRequired: true,
selectedItem: _value,
onSelected: (v) => setState(() => _value = v),
validator: (val) => val == null ? 'Please select an option' : null,
autovalidateMode: AutovalidateMode.onUserInteraction,
),
ElevatedButton(
onPressed: () => _formKey.currentState!.validate(),
child: const Text('Submit'),
),
],
),
)
6 ยท Custom colours & style
PopupDropdown<String>(
items: items,
labelExtractor: (i) => i,
hint: 'Pick one',
selectedItem: _value,
onSelected: (v) => setState(() => _value = v),
// Colours
fillColor: Colors.indigo.shade50,
borderColor: Colors.indigo.shade200,
focusedBorderColor: Colors.indigo,
errorBorderColor: Colors.red.shade300,
// Shape
borderRadius: 20,
height: 52,
borderWidth: 1.5,
// Icon
suffixIcon: Icons.expand_more,
suffixIconColor: Colors.indigo,
rotateSuffixIcon: true,
// Text
hintStyle: TextStyle(color: Colors.grey.shade400),
selectedStyle: TextStyle(
color: Colors.indigo,
fontWeight: FontWeight.w700,
),
// Menu
menuColor: Colors.white,
menuElevation: 8,
selectedCheckColor: Colors.indigo,
showDividers: false,
)
7 ยท Custom child trigger
Replace the default trigger field with any widget:
PopupDropdown<String>(
items: items,
labelExtractor: (i) => i,
hint: 'Pick',
onSelected: (v) => ...,
customChild: ElevatedButton.icon(
onPressed: null, // PopupMenuButton handles the tap
icon: const Icon(Icons.filter_list),
label: Text(_value ?? 'Filter'),
),
)
8 ยท Disabled state
PopupDropdown<String>(
items: items,
labelExtractor: (i) => i,
hint: 'Cannot change',
selectedItem: 'Locked value',
enabled: false,
disabledOpacity: 0.5,
onSelected: (_) {},
)
9 ยท With BLoC / state management
BlocBuilder<CategoryBloc, CategoryState>(
builder: (context, state) {
return PopupDropdown<Category>(
items: state.categories,
labelExtractor: (c) => c.name,
title: 'Category',
isRequired: true,
selectedItem: state.selectedCategory,
hint: 'Choose category',
onSelected: (c) =>
context.read<CategoryBloc>().add(CategorySelected(c)),
validator: (v) => v == null ? 'Required' : null,
);
},
)
API Reference
Required parameters
| Parameter | Type | Description |
|---|---|---|
items |
List<T> |
Items to display |
labelExtractor |
String Function(T) |
Extracts display string from item |
Commonly used optional parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
selectedItem |
T? |
null | Currently selected item |
onSelected |
void Function(T)? |
null | Selection callback |
hint |
String |
'Select an option' |
Placeholder text |
title |
String? |
null | Label above the field |
isRequired |
bool |
false | Shows red asterisk |
enabled |
bool |
true | Enable/disable interaction |
validator |
String? Function(T?)? |
null | Form validation |
autovalidateMode |
AutovalidateMode? |
null | When to auto-validate |
Styling
| Parameter | Type | Default | Description |
|---|---|---|---|
fillColor |
Color? |
White | Field background |
borderColor |
Color? |
Theme divider | Normal border |
focusedBorderColor |
Color? |
Theme primary | Open border |
errorBorderColor |
Color? |
Theme error | Error border |
borderRadius |
double |
12.0 |
Corner radius |
borderWidth |
double? |
1.0 / 1.5 |
Border thickness |
height |
double |
56.0 |
Trigger field height |
padding |
EdgeInsetsGeometry? |
bottom 20 | Outer padding |
titleStyle |
TextStyle? |
null | Label text style |
hintStyle |
TextStyle? |
null | Hint text style |
selectedStyle |
TextStyle? |
null | Selected value style |
itemStyle |
TextStyle? |
null | Item text style |
selectedItemStyle |
TextStyle? |
null | Selected item style in menu |
Icon
| Parameter | Type | Default | Description |
|---|---|---|---|
suffixIcon |
IconData |
keyboard_arrow_down_rounded |
Trailing icon |
suffixIconColor |
Color? |
Theme hint | Icon colour |
rotateSuffixIcon |
bool |
true | Rotate 180ยฐ when open |
Menu
| Parameter | Type | Default | Description |
|---|---|---|---|
offset |
Offset |
Offset(0, 4) |
Menu position offset |
menuConstraints |
BoxConstraints? |
null | Override menu size |
maxMenuHeight |
double |
300 |
Max popup height |
menuColor |
Color? |
null | Menu background |
menuElevation |
double |
3 |
Menu shadow |
itemHeight |
double |
48 |
Row height |
itemPadding |
EdgeInsetsGeometry |
Horizontal 16 | Row padding |
selectedCheckColor |
Color? |
Theme primary | Check icon colour |
showDividers |
bool |
true | Dividers between items |
dividerColor |
Color? |
Theme divider | Divider colour |
Advanced
| Parameter | Type | Description |
|---|---|---|
customChild |
Widget? |
Replace default trigger entirely |
itemLeadingBuilder |
Widget? Function(T)? |
Leading widget per item |
itemTrailingBuilder |
Widget? Function(T, bool)? |
Trailing widget per item |
disabledOpacity |
double |
Opacity when disabled (default 0.6) |
onTap |
VoidCallback? |
Tap callback when disabled |
Common Use Cases
โ
Category / product type pickers
โ
Country / region selection
โ
Status / priority selectors
โ
Sort / filter options
โ
Settings form fields
โ
Any single-select dropdown
Contributing
Pull requests are welcome! For major changes, please open an issue first.
License
MIT โ see LICENSE.
Libraries
- popup_dropdown
- A highly customizable popup dropdown widget for Flutter with form validation, animated icon, error states, and zero external dependencies.