Flutter Dropdown Button

A highly customizable dropdown package for Flutter with overlay-based rendering, smooth animations, and full control over appearance and behavior.

pub package License: MIT

Features

  • Two Widgets: FlutterDropdownButton<T> chooses one item; FlutterMultiSelectDropdown<T> is a checklist that stays open
  • Two Modes: Custom widget rendering (default) or text-only (.text())
  • Multi-Select: Tick boxes, get a Set<T> at once — anchored, not modal, and no confirm button
  • Any Type in Text Mode: Supply a label callback and keep tooltips, overflow handling and search
  • Overlay-based Rendering: Better positioning and visual effects than Flutter's built-in DropdownButton
  • Smart Positioning: Automatically opens up/down based on available space
  • Smooth Animations: Scale and fade effects with configurable timing
  • Outside-tap Dismissal: Automatic closure when tapping outside
  • Flexible Width: Fixed, min/max constraints, content-based, or flex expansion
  • Independent Menu Width: Set menu width separately from button with alignment control
  • Text Overflow Control: Ellipsis, fade, clip, or visible overflow options
  • Smart Tooltip: Automatic tooltip on overflow with full customization
  • Custom Scrollbar: Scrollbar theming with colors, thickness, and visibility
  • Single-Item Mode: Auto-disable when only one option exists
  • Leading Widgets: Optional icons/widgets before text content
  • Searchable Dropdown: Real-time filtering with customizable search field
  • Build Your Own: DropdownOverlayController exposes the overlay machinery, and TextItemPresentation the text rendering

Screenshots

Basic Text Dropdown Icon + Text Dropdown with Hover
Basic Text Dropdown
Simple text options with customizable styles
Icon + Text Dropdown
Rich content with icons and hover effects

Quick Start

Add to your pubspec.yaml:

dependencies:
  flutter_dropdown_button: ^3.2.0

Import the package:

import 'package:flutter_dropdown_button/flutter_dropdown_button.dart';

Basic Usage

Text Dropdown (Fixed Width)

FlutterDropdownButton<String>.text(
  items: ['Apple', 'Banana', 'Cherry'],
  value: selectedValue,
  hint: 'Select a fruit',
  width: 200,
  onChanged: (value) {
    setState(() => selectedValue = value);
  },
)

Text Dropdown (Dynamic Width)

FlutterDropdownButton<String>.text(
  items: ['Apple', 'Banana', 'Cherry'],
  value: selectedValue,
  hint: 'Select a fruit',
  minWidth: 120,
  maxWidth: 300,
  onChanged: (value) {
    setState(() => selectedValue = value);
  },
)

Text Dropdown of a Domain Type

Pass a label to render any type as text. Overflow handling, the tooltip, and the default search filter all work off the label.

FlutterDropdownButton<User>.text(
  items: users,
  value: selectedUser,
  label: (user) => user.name,
  hint: 'Select a user',
  searchable: true,          // filters by name, no searchFilter needed
  onChanged: (value) {
    setState(() => selectedUser = value);
  },
)

Reach for the default constructor only when an item needs more than text — an avatar, an icon, a two-line layout.

Custom Widget Dropdown

FlutterDropdownButton<String>(
  items: ['home', 'settings', 'profile'],
  value: selectedValue,
  hintWidget: Text('Choose option'),
  itemBuilder: (item, isSelected) => Row(
    children: [
      Icon(_getIcon(item), size: 20),
      SizedBox(width: 8),
      Text(item),
    ],
  ),
  onChanged: (value) {
    setState(() => selectedValue = value);
  },
)

Custom Selected Display

FlutterDropdownButton<String>(
  items: ['apple', 'banana'],
  value: selectedValue,
  itemBuilder: (item, isSelected) => Text(item),
  selectedBuilder: (item) => Text(item.toUpperCase(),
    style: TextStyle(fontWeight: FontWeight.bold),
  ),
  onChanged: (value) {
    setState(() => selectedValue = value);
  },
)

Multi-Select Checklist

Ticking a box calls onChanged immediately with a new Set. The menu stays open, so the next box can be ticked. There is no confirm button and no scrim.

FlutterMultiSelectDropdown<String>(
  items: ['Windows', 'Linux', 'macOS'],
  selected: chosen,
  labelBuilder: (s) => switch (s.length) {
    0 => 'All',
    1 => s.first,
    _ => '${s.length} selected',
  },
  searchable: true,                        // when the list runs long
  itemLeadingBuilder: (v) => Icon(osIcon[v]),
  itemTrailingBuilder: (v) => Text('${counts[v]}'),
  onChanged: (next) => setState(() => chosen = next),
)

selected is yours. The widget copies it before emitting, never edits it, and draws whatever you hand it — see A value that is not in items.


API Reference

FlutterDropdownButton<T>

The unified dropdown widget. Use the default constructor for custom widget rendering, or .text() for text-only content.

Custom Mode (Default Constructor)

Parameter Type Default Description
items List<T> required List of item values
onChanged ValueChanged<T?> required Called when an item is selected
itemBuilder Widget Function(T, bool) required Builds widget for each item (item, isSelected)
selectedBuilder Widget Function(T)? null Builds widget for selected item on button face (falls back to itemBuilder)
hintWidget Widget? null Widget shown when no item is selected

Text Mode (.text Constructor)

Parameter Type Default Description
items List<T> required List of item values
onChanged ValueChanged<T?> required Called when an item is selected
hint String? null Text shown when no item is selected
label String Function(T)? null Extracts the text to display for an item. Required unless T is String
config TextDropdownConfig? null Text rendering configuration
leading Widget? null Widget before text in all items
selectedLeading Widget? null Widget before text in selected item (falls back to leading)
leadingPadding EdgeInsets? right: 8.0 Padding around the leading widget

Common Parameters (Both Modes)

Parameter Type Default Description
value T? null Currently selected value. Drawn on the button whether or not it is in items — see below
width double? null Fixed width (null = content-based)
minWidth double? null Minimum width constraint
maxWidth double? null Maximum width constraint
height double 200.0 Maximum height of dropdown overlay
itemHeight double 48.0 Height of each dropdown item
animationDuration Duration 200ms Duration of show/hide animation
enabled bool true Whether the dropdown is interactive
expand bool false Expand to fill available space in flex container
trailing Widget? null Custom widget replacing default arrow icon
scrollToSelectedItem bool true Auto-scroll to selected item on open
scrollToSelectedDuration Duration? null Scroll animation duration (null = instant jump)
disableWhenSingleItem bool false Disable dropdown when only one item exists
hideIconWhenSingleItem bool true Hide arrow icon in single-item mode
minMenuWidth double? null Minimum width of dropdown menu
maxMenuWidth double? null Maximum width of dropdown menu
menuAlignment MenuAlignment .left Menu alignment when wider than button
theme DropdownStyleTheme? null Theme configuration
searchable bool false Enable search/filter field in dropdown
searchFilter bool Function(T, String)? null Custom filter function (required for custom mode)
emptyBuilder Widget Function(String)? null Widget builder for empty search results

A value that is not in items

A list refresh can drop the row a value names while value still names it. The button keeps drawing that value; the menu, which iterates items, draws no row for it. The widget draws what it was handedvalue is yours, and a button that silently reverted to its hint would disagree with the state you hold. Nothing throws.

FlutterMultiSelectDropdown follows the same rule: such a value still counts towards labelBuilder, so '3 selected' can appear beside two ticked rows. Give the user a way to clear it.

FlutterMultiSelectDropdown<T>

A checklist. Several items may be chosen, the menu stays open while they are, and onChanged fires the moment a box is ticked. Anchored rather than modal: no scrim, dismissed by an outside tap.

Rows render as text, so T must be a String or label must say how to make one. T must implement == and hashCode consistently — a Set needs both, where single-select's value == item needed only the first.

Parameter Type Default Description
items List<T> required List of item values
selected Set<T> required The chosen items. Yours; never mutated
onChanged ValueChanged<Set<T>> required Called with a new Set the moment a row is tapped
labelBuilder String Function(Set<T>) required Turns selected into the button's face
label String Function(T)? null The text a row shows. Required unless T is String
itemLeadingBuilder Widget Function(T)? null Drawn between the checkbox and the label — an icon that varies by item
itemTrailingBuilder Widget Function(T)? null Drawn at the end of each row — a count, a badge
config TextDropdownConfig? null Text rendering configuration

Everything under Common Parameters above applies too, except the five that mean nothing to a checklist and therefore do not exist on it: value, scrollToSelectedItem, scrollToSelectedDuration, disableWhenSingleItem and hideIconWhenSingleItem. They are absent from the type rather than asserted against at runtime.

FlutterMultiSelectDropdown.closeAll();   // the same registry as the other widget

Alignment of the dropdown menu relative to the button when the menu is wider.

Value Description
MenuAlignment.left Left edges align, menu extends right (default)
MenuAlignment.center Menu centered over button
MenuAlignment.right Right edges align, menu extends left

Theme System

Theme is applied via the theme parameter using DropdownStyleTheme, which groups five theme objects:

DropdownStyleTheme(
  dropdown: DropdownTheme(...),          // General dropdown styling
  scroll: DropdownScrollTheme(...),      // Scrollbar styling
  tooltip: DropdownTooltipTheme(...),    // Tooltip styling
  search: SearchFieldTheme(...),         // Search field styling
  checkbox: DropdownCheckboxTheme(...),  // Multi-select row checkbox styling
)

Controls general styling for button, overlay, and items.

The theme fills in its own gaps. resolveButton(), resolveOverlay() and resolveItem() return styles whose slots are all filled, taking a plain DropdownAmbientColors palette rather than a BuildContext — so the rule for "what colour is the arrow when disabled?" lives in one place and can be tested without a widget. You rarely call these directly; the widget does.

Button Styling

Parameter Type Default Description
buttonDecoration BoxDecoration? null Custom button decoration (overrides all below)
buttonPadding EdgeInsets horizontal: 16, vertical: 12 Internal padding of button
buttonHeight double? null Height of button content area (falls back to iconSize or 24.0)
buttonHoverColor Color? null Button hover background color
buttonSplashColor Color? null Button tap ripple color
buttonHighlightColor Color? null Button focus highlight color

Overlay Styling

Parameter Type Default Description
overlayDecoration BoxDecoration? null Custom overlay decoration (overrides backgroundColor, border, borderRadius)
overlayPadding EdgeInsets? null Padding inside the overlay container
borderRadius double 8.0 Border radius for button and overlay
elevation double 8.0 Shadow depth of overlay
backgroundColor Color? null Overlay background color (falls back to Theme.cardColor)
border Border? null Border for button and overlay (falls back to Theme.dividerColor)
shadowColor Color? null Overlay shadow color

Item Styling

Parameter Type Default Description
itemPadding EdgeInsets horizontal: 16, vertical: 12 Internal padding of each item
itemMargin EdgeInsets? null External margin around each item
itemBorderRadius double? null Border radius for individual items
itemBorder Border? null Border for each item (e.g., bottom divider)
excludeLastItemBorder bool true Skip itemBorder on the last item
selectedItemColor Color? null Background color for selected item (falls back to primaryColor 10%)
itemHoverColor Color? null Item hover background color
itemSplashColor Color? null Item tap ripple color
itemHighlightColor Color? null Item focus highlight color

Icon Styling

Parameter Type Default Description
icon IconData? Icons.keyboard_arrow_down Dropdown arrow icon
iconSize double? 24.0 Size of the dropdown icon
iconColor Color? null Icon color when enabled
iconDisabledColor Color? null Icon color when disabled
iconPadding EdgeInsets? EdgeInsets.only(left: 8.0) Padding around the icon

Disabled State

Applied when enabled: false (or when the single-item auto-disable kicks in). If none of these are set, the disabled button falls back to the regular buttonDecoration / border.

Parameter Type Default Description
disabledButtonDecoration BoxDecoration? null Full custom decoration for the disabled button (takes precedence over the two below)
disabledBackgroundColor Color? null Background color of the button when disabled
disabledBorder Border? null Border of the button when disabled (falls back to border)

Controls scrollbar appearance inside the dropdown overlay.

Parameter Type Default Description
thumbWidth double? null Width of the scrollbar. Wins over thickness
thickness double? null Thickness of the scrollbar. Unset, the ambient ScrollbarTheme decides, then Flutter's own default — 8 on desktop, 4 on Android
radius Radius? null Corner radius of scrollbar thumb. Unset, Radius.circular(8) on desktop and square on Android
thumbColor Color? null Color of the scrollbar thumb
trackColor Color? null Color of the scrollbar track. Paints nothing unless trackVisibility is true
trackBorderColor Color? null Border color of the scrollbar track, on the same terms as trackColor
thumbVisibility bool? null true pins the thumb on screen. false and null behave alike: it fades in while scrolling and fades out
trackVisibility bool? null true draws the track and pins the thumb with it. Unset, there is no track
interactive bool? null false makes the thumb undraggable. Unset, Flutter decides — draggable on desktop, not on Android
crossAxisMargin double? null Margin from edge of scroll view
mainAxisMargin double? null Margin at top/bottom of scrollbar
minThumbLength double? null Minimum length of scrollbar thumb
showScrollGradient bool? false Show fade gradient when scrollable
gradientHeight double? 24.0 Height of gradient effect
gradientColors List<Color>? null Custom gradient colors (auto-detects from background if null)

A colour is not a request. trackColor describes the track; trackVisibility is what draws it. A null bool? means let the ambient ScrollbarTheme decide — not off.

Controls tooltip styling and behavior for text-based dropdowns.

Visual Styling

Parameter Type Default Description
decoration Decoration? null Custom tooltip decoration; a total override
backgroundColor Color? null Tooltip background color
textColor Color? null Tooltip text color
textStyle TextStyle? null Tooltip text style (overrides textColor)
borderRadius BorderRadius? null Tooltip corner radius
border BoxBorder? null Tooltip border
shadow List<BoxShadow>? null Tooltip shadow
padding EdgeInsetsGeometry? null Padding inside tooltip
margin EdgeInsetsGeometry? null Margin around tooltip

Setting any of backgroundColor, borderRadius, border or shadow hands this theme the tooltip's whole box; the ones you leave unset fall back to Flutter's own tooltip defaults. Set none of them and an app-wide TooltipTheme still applies.

Behavior

Parameter Type Default Description
enabled bool true Enable/disable tooltip
mode TooltipMode .onlyWhenOverflow When to show tooltip
waitDuration Duration 500ms Delay before showing on hover
showDuration Duration 3s How long tooltip stays visible
verticalOffset double? null Gap between widget and tooltip
triggerMode TooltipTriggerMode? null How tooltip is triggered

SearchFieldTheme

Controls the appearance and behavior of the search text field when searchable is enabled.

Layout

Parameter Type Default Description
height double? 36.0 Height of the search field
borderRadius BorderRadius? circular(8) Corner radius of the search field
margin EdgeInsets? fromLTRB(8, 8, 8, 4) Outer margin around the search field
padding EdgeInsets? null Inner padding of the search field container
contentPadding EdgeInsets? horizontal: 12, vertical: 8 Content padding inside the text field

Styling

Parameter Type Default Description
decoration InputDecoration? null Full InputDecoration override (ignores individual properties when set)
textStyle TextStyle? null Text style for search input
backgroundColor Color? null Background color of the search field
border BoxBorder? null Border when not focused
focusedBorder BoxBorder? null Border when focused
divider Widget? null Widget between search field and item list

Cursor

Parameter Type Default Description
cursorColor Color? null Cursor color
cursorWidth double? 2.0 Cursor width
cursorHeight double? null Cursor height
cursorRadius Radius? null Cursor corner radius

Behavior

Parameter Type Default Description
autofocus bool true Auto-focus search field on dropdown open
textAlign TextAlign .start Text alignment
keyboardType TextInputType? .text Keyboard type
textInputAction TextInputAction? .search Keyboard action button

Styles the checkbox on each row of a FlutterMultiSelectDropdown. Reaches the box even though the menu is drawn in the root overlay, where a Theme wrapped around the widget cannot — only an app-wide CheckboxThemeData otherwise could, and that restyles every checkbox in the app. Only the fields that render on the presentational (onChanged: null, semantics-excluded) box are here.

Parameter Type Default Description
activeColor Color? null Fill of a checked box. Routed through fillColor so it survives the box's disabled state
fillColor WidgetStateProperty<Color?>? null Per-state fill, for full control. Wins over activeColor
checkColor Color? null Color of the checkmark
side BorderSide? null Outline of an unchecked box (a checked box's fill covers it)
shape OutlinedBorder? null Box shape, e.g. rounded corners
materialTapTargetSize MaterialTapTargetSize? null Size class the box occupies
visualDensity VisualDensity? null Density adjustment to the box's size
mouseCursor MouseCursor? null Cursor over the box. Set to SystemMouseCursors.click to match the row's

Each null field defers to the ambient CheckboxThemeData.

TextDropdownConfig

Configuration for text rendering in .text() mode.

Parameter Type Default Description
overflow TextOverflow .ellipsis How to handle text overflow
maxLines int? 1 Maximum number of lines
textStyle TextStyle? null Style for item text
hintStyle TextStyle? null Style for hint text
selectedTextStyle TextStyle? null Style for selected item text
disabledTextStyle TextStyle? null Style for button text (value and hint) when disabled — merged over textStyle / hintStyle
textAlign TextAlign .start Horizontal text alignment (also controls item alignment in menu)
softWrap bool true Allow line breaks at word boundaries

Advanced Features

Searchable Dropdown (Text Mode)

FlutterDropdownButton<String>.text(
  items: ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'],
  value: selected,
  hint: 'Select a fruit',
  width: 250,
  searchable: true,
  onChanged: (value) => setState(() => selected = value),
)

Searchable Dropdown (Custom Mode)

FlutterDropdownButton<User>(
  items: users,
  value: selectedUser,
  searchable: true,
  searchFilter: (user, query) =>
    user.name.toLowerCase().contains(query.toLowerCase()),
  emptyBuilder: (query) => Center(
    child: Text('No users matching "$query"'),
  ),
  itemBuilder: (user, isSelected) => Row(
    children: [
      CircleAvatar(radius: 14, child: Text(user.name[0])),
      SizedBox(width: 8),
      Text(user.name),
    ],
  ),
  onChanged: (value) => setState(() => selectedUser = value),
)

Searchable Dropdown with Custom Theme

FlutterDropdownButton<String>.text(
  items: countries,
  value: selected,
  searchable: true,
  theme: DropdownStyleTheme(
    search: SearchFieldTheme(
      backgroundColor: Colors.grey.shade100,
      height: 40,
      borderRadius: BorderRadius.circular(12),
      cursorColor: Colors.blue,
      divider: Divider(height: 1),
    ),
  ),
  onChanged: (value) => setState(() => selected = value),
)

Custom Theme

FlutterDropdownButton<String>.text(
  items: items,
  value: selected,
  width: 200,
  theme: DropdownStyleTheme(
    dropdown: DropdownTheme(
      borderRadius: 12.0,
      elevation: 4.0,
      backgroundColor: Colors.white,
      selectedItemColor: Colors.blue.withOpacity(0.1),
      itemHoverColor: Colors.grey.withOpacity(0.1),
      itemBorder: Border(bottom: BorderSide(color: Colors.grey.shade300)),
      excludeLastItemBorder: true,
    ),
    scroll: DropdownScrollTheme(
      thumbWidth: 6.0,
      thumbColor: Colors.blue,
      showScrollGradient: true,
    ),
    tooltip: DropdownTooltipTheme(
      backgroundColor: Colors.black87,
      textStyle: TextStyle(color: Colors.white),
      borderRadius: BorderRadius.circular(8),
      mode: TooltipMode.onlyWhenOverflow,
    ),
  ),
  onChanged: (value) => setState(() => selected = value),
)
FlutterDropdownButton<String>.text(
  items: items,
  width: 120,
  minMenuWidth: 250,
  maxMenuWidth: 400,
  menuAlignment: MenuAlignment.center,
  onChanged: (value) {},
)

Single-Item Mode

FlutterDropdownButton<String>.text(
  items: ['Only Option'],
  disableWhenSingleItem: true,
  hideIconWhenSingleItem: true,
  onChanged: (value) {},
)

Leading Widget

FlutterDropdownButton<String>.text(
  items: ['USD', 'EUR', 'JPY'],
  leading: Icon(Icons.attach_money, size: 20),
  selectedLeading: Icon(Icons.attach_money, size: 20, color: Colors.blue),
  leadingPadding: EdgeInsets.only(right: 12),
  onChanged: (value) {},
)

Expand in Flex Container

Row(
  children: [
    Text('Label:'),
    FlutterDropdownButton<String>.text(
      items: items,
      expand: true,
      maxWidth: 200,
      onChanged: (value) {},
    ),
  ],
)

Manual Dropdown Cleanup

// Close with animation (trailing icon rotates back)
FlutterDropdownButton.closeAll();

// Close immediately without animation (useful before navigation)
FlutterDropdownButton.closeAll(animate: false);
Navigator.pushNamedAndRemoveUntil(context, '/home', (route) => false);

Note: Dropdowns are automatically cleaned up during widget disposal. Use closeAll() only when you need explicit control.


Migration from 1.x

BasicDropdownButton → FlutterDropdownButton

// Before (1.x)
BasicDropdownButton<String>(
  items: [
    DropdownItem(value: 'apple', child: Text('Apple'), onTap: () {}),
    DropdownItem(value: 'banana', child: Text('Banana')),
  ],
  value: selected,
  hint: Text('Select'),
  onChanged: (v) {},
)

// After (2.0)
FlutterDropdownButton<String>(
  items: ['apple', 'banana'],
  value: selected,
  hintWidget: Text('Select'),
  itemBuilder: (item, isSelected) => Text(item),
  onChanged: (v) {},
)

TextOnlyDropdownButton → FlutterDropdownButton.text

// Before (1.x)
TextOnlyDropdownButton(
  items: ['A', 'B', 'C'],
  value: selected,
  hint: 'Select',
  width: 200,
  onChanged: (v) {},
)

// After (2.0)
FlutterDropdownButton<String>.text(
  items: ['A', 'B', 'C'],
  value: selected,
  hint: 'Select',
  width: 200,
  onChanged: (v) {},
)

DynamicTextBaseDropdownButton → FlutterDropdownButton.text

// Before (1.x)
DynamicTextBaseDropdownButton(
  items: items,
  value: selected,
  disableWhenSingleItem: true,
  leading: Icon(Icons.star),
  onChanged: (v) {},
)

// After (2.0)
FlutterDropdownButton<String>.text(
  items: items,
  value: selected,
  disableWhenSingleItem: true,
  leading: Icon(Icons.star),
  onChanged: (v) {},
)

Removed APIs

Removed Replacement
DropdownItem<T> itemBuilder callback
showSeparator / separator DropdownTheme.itemBorder
BasicDropdownButton.borderRadius DropdownTheme.borderRadius
BasicDropdownButton.decoration DropdownTheme.overlayDecoration
DropdownItem.onTap Handle in onChanged callback

Example

Check out the example app for a comprehensive demonstration of all features and customization options.

Documentation

License

This project is licensed under the MIT License - see the LICENSE file for details.

Changelog

See CHANGELOG.md for a detailed list of changes and version history.