popup_dropdown

A highly customizable popup dropdown widget for Flutter with form validation, animated icon rotation, error states, and zero external dependencies.

pub package

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
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

A highly customizable popup dropdown widget for Flutter with form validation, animated icon, error states, and zero external dependencies.