Item Dropper
A customizable, accessible dropdown package for Flutter with powerful single-select and multi-select capabilities, built-in search filtering, and full keyboard navigation support.
πΈ Screenshots
Single-Select Dropdown

Multi-Select Dropdown with Chips

β¨ Features
Core Functionality
- β Single-Select Dropdown - Traditional dropdown with single item selection
- β Multi-Select Dropdown - Select multiple items displayed as chips
- β Real-time Search - Filter items as you type
- β Keyboard Navigation - Full support for arrow keys, Enter, and Escape
- β Add New Items - Allow users to create new items on-the-fly
- β Delete Items - Optional delete buttons for managing the item list
- β Group Headers - Organize items with visual separators
- β Smart Positioning - Automatically positions dropdown above or below based on screen space
Customization
- π¨ Fully Styleable - Custom text styles, decorations, and item builders
- π― Max Selection Limit - Optionally limit number of selections in multi-select
- βοΈ Flexible Configuration - Width, height, elevation, and more
- π Custom Item Rendering - Provide your own popup item builder
- ποΈ Custom Decorations - Customize field container and chip appearance
Accessibility
- βΏ Screen Reader Support - Full semantic labels and live region announcements
- β¨οΈ Keyboard Accessible - Navigate and select without a mouse
- π Selection Announcements - Screen readers announce item selections
Performance
- β‘ Optimized Filtering - Cached results for fast search
- π¦ Smart Rebuilds - Efficient state management prevents unnecessary rebuilds
- πΎ Decoration Caching - Pre-computed decorations for smooth rendering
π¦ Installation
Add item_dropper to your pubspec.yaml:
dependencies:
item_dropper: ^1.0.0
Then run:
flutter pub get
π Quick Start
Single-Select Dropdown
import 'package:item_dropper/item_dropper.dart';
// Create your items
final items = [
ItemDropperItem(value: '1', label: 'Apple'),
ItemDropperItem(value: '2', label: 'Banana'),
ItemDropperItem(value: '3', label: 'Orange'),
ItemDropperItem(value: '4', label: 'Grapes'),
];
// Track selected item
ItemDropperItem<String>? selectedItem;
// Use in your widget tree
SingleItemDropper<String>(
items: items,
selectedItem: selectedItem,
width: 300,
onChanged: (item) {
setState(() {
selectedItem = item;
});
},
)
selectedItem supplies the initial selection and subsequent parent updates.
Changing it updates the dropdown; changing a non-null value to null clears both
selection and text. Unchanged values, including null or an omitted argument,
preserve the user's selection across parent rebuilds. Parent-driven changes do
not invoke onChanged.
To clear a selection through this parameter, keep the parent variable updated
from onChanged, as above, then set it to null. A variable that stays null cannot
signal a new clear request.
Multi-Select Dropdown
import 'package:item_dropper/item_dropper.dart';
// Create your items
final items = [
ItemDropperItem(value: '1', label: 'Red'),
ItemDropperItem(value: '2', label: 'Blue'),
ItemDropperItem(value: '3', label: 'Green'),
ItemDropperItem(value: '4', label: 'Yellow'),
];
// Track selected items
List<ItemDropperItem<String>> selectedItems = [];
// Use in your widget tree
MultiItemDropper<String>(
items: items,
selectedItems: selectedItems,
width: 400,
maxSelected: 3, // Optional: limit selections
onChanged: (items) {
setState(() {
selectedItems = items;
});
},
)
π― Common Use Cases
With Search/Filter
Both widgets automatically support search - just start typing!
SingleItemDropper<String>(
items: longListOfItems,
selectedItem: selectedItem,
width: 300,
onChanged: (item) => setState(() => selectedItem = item),
// Search is built-in - no configuration needed!
)
With Group Headers
final itemsWithGroups = [
ItemDropperItem(value: 'header1', label: 'Fruits', isGroupHeader: true),
ItemDropperItem(value: '1', label: 'Apple'),
ItemDropperItem(value: '2', label: 'Banana'),
ItemDropperItem(value: 'header2', label: 'Vegetables', isGroupHeader: true),
ItemDropperItem(value: '3', label: 'Carrot'),
ItemDropperItem(value: '4', label: 'Lettuce'),
];
SingleItemDropper<String>(
items: itemsWithGroups,
selectedItem: selectedItem,
width: 300,
onChanged: (item) => setState(() => selectedItem = item),
)
Allow Adding New Items
Both dropdowns refresh their item caches when the parent rebuilds, including
after in-place list changes such as setState(() => items.add(newItem)).
Replacing the list also works. Item rows are immutable; replace a row to update
its label or other properties.
Detecting these changes takes O(n) item comparisons per parent rebuild and an O(n) saved snapshot per dropdown. Account for this cost with very large lists.
Both dropdowns support onAddItem with an initially empty items list. The
callback runs when the user selects the add row, and may return null to cancel.
Custom popup builders must check item.isAddItem before reading item.value:
add rows are actions with a label, not values of your item type. Reading .value
on an add row throws a StateError; render its label instead.
SingleItemDropper<String>(
items: items,
selectedItem: selectedItem,
width: 300,
onChanged: (item) => setState(() => selectedItem = item),
onAddItem: (searchText) {
// Create new item from search text
final newItem = ItemDropperItem(
value: searchText.toLowerCase(),
label: searchText,
);
// Add to your items list
setState(() {
items.add(newItem);
});
// Return the new item to auto-select it
return newItem;
},
)
Custom Styling
MultiItemDropper<String>(
items: items,
selectedItems: selectedItems,
width: 400,
onChanged: (items) => setState(() => selectedItems = items),
// Customize field text style
fieldTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.blue.shade900,
),
// Customize popup text style
popupTextStyle: TextStyle(
fontSize: 12,
color: Colors.black87,
),
// Customize group header style
popupGroupHeaderStyle: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
// Custom chip decoration
selectedChipDecoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(16),
),
// Custom field decoration
fieldDecoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.blue, width: 2),
borderRadius: BorderRadius.circular(12),
),
)
Popup Elevation and Scrollbar
Both dropdowns use elevation for the popup's Material shadow (default 4.0);
0 removes the shadow. In multi-select, this also applies to the no-results and
maximum-selection messages. A null multi-select elevation uses the default.
For list popups, showScrollbar controls persistent thumb visibility (default
true); false lets the scrollbar appear while scrolling and fade afterward.
scrollbarThickness sets its width in logical pixels (default 6.0). These
settings and elevation update when the parent rebuilds, including while open.
MultiItemDropper<String>(
items: items,
width: 300,
selectedItems: selectedItems,
elevation: 8,
showScrollbar: true,
scrollbarThickness: 10,
onChanged: (items) => setState(() => selectedItems = items),
)
Disabled State
SingleItemDropper<String>(
items: items,
selectedItem: selectedItem,
width: 300,
enabled: false, // Disables interaction
onChanged: (item) => setState(() => selectedItem = item),
)
π Advanced Features
Custom Item Rendering
Provide your own popup item builder for complete control:
SingleItemDropper<String>(
items: items,
selectedItem: selectedItem,
width: 300,
onChanged: (item) => setState(() => selectedItem = item),
popupItemBuilder: (context, item, isSelected) {
return Container(
padding: EdgeInsets.all(12),
color: isSelected ? Colors.blue.shade100 : Colors.white,
child: Row(
children: [
Icon(Icons.star, color: Colors.amber),
SizedBox(width: 8),
Text(item.label),
],
),
);
},
)
Programmatic Focus Control
final inputKey = GlobalKey();
SingleItemDropper<String>(
inputKey: inputKey,
items: items,
selectedItem: selectedItem,
width: 300,
onChanged: (item) => setState(() => selectedItem = item),
)
// Later, programmatically focus:
FocusScope.of(context).requestFocus(
FocusNode()..requestFocus(),
);
Keyboard Navigation
Built-in keyboard support:
- β/β Arrow Keys - Navigate through items
- Enter - Select highlighted item
- Escape - Close dropdown
- Type to search - Filter items in real-time
Localization
All user-facing text can be customized by providing an ItemDropperLocalizations instance:
SingleItemDropper<String>(
items: items,
selectedItem: selectedItem,
width: 300,
onChanged: (item) => setState(() => selectedItem = item),
localizations: ItemDropperLocalizations(
addItemPrefix: 'Ajouter "',
addItemSuffix: '"',
noResultsFound: 'Aucun rΓ©sultat trouvΓ©',
deleteDialogTitle: 'Supprimer "{label}"?',
deleteDialogContent: 'Cet Γ©lΓ©ment sera supprimΓ© de la liste.',
deleteDialogCancel: 'Annuler',
deleteDialogDelete: 'Supprimer',
// ... other strings
),
)
See ItemDropperLocalizations class for all available strings.
π¨ Customization Options
SingleItemDropper Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
items |
List<ItemDropperItem<T>> |
required | The items to display in the dropdown |
selectedItem |
ItemDropperItem<T>? |
null |
The currently selected item |
onChanged |
Function(ItemDropperItem<T>?) |
required | Called when the selection changes |
popupItemBuilder |
Widget Function(...)? |
null |
Optional custom builder for popup items |
width |
double |
required | The width of the dropdown field |
enabled |
bool |
true |
Whether the dropdown is enabled |
hintText |
String? |
null |
Hint/placeholder text for input field |
showKeyboard |
bool |
false |
Whether to show the mobile keyboard |
onAddItem |
ItemDropperItem<T>? Function(String)? |
null |
Callback for adding new items based on search text entered by user |
onDeleteItem |
Function(ItemDropperItem<T>)? |
null |
Callback for deleting items |
maxDropdownHeight |
double |
200.0 |
Maximum dropdown popup height |
elevation |
double |
4.0 |
Popup shadow elevation |
showScrollbar |
bool |
true |
Keep the popup scrollbar thumb visible |
scrollbarThickness |
double |
6.0 |
Popup vertical scrollbar thickness |
fieldTextStyle |
TextStyle? |
null |
Text style for input/search field |
popupTextStyle |
TextStyle? |
null |
Text style for popup dropdown items |
popupGroupHeaderStyle |
TextStyle? |
null |
Text style for group headers in popup |
itemHeight |
double? |
null |
Height for popup dropdown items |
fieldDecoration |
BoxDecoration? |
null |
Optional BoxDecoration for field container |
showDropdownPositionIcon |
bool |
true |
Show the dropdown position arrow (down/up) |
showDeleteAllIcon |
bool |
true |
Show the clear (X) icon |
localizations |
ItemDropperLocalizations? |
null |
Localization strings for user-facing text (optional) |
inputKey |
GlobalKey? |
null |
Key for programmatic access |
dependencies |
SingleItemDropperDependencies<T>? |
null |
Dependency factory hooks for tests and advanced integrations |
MultiItemDropper Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
items |
List<ItemDropperItem<T>> |
required | The items to display in the dropdown |
selectedItems |
List<ItemDropperItem<T>>? |
null |
The currently selected items |
onChanged |
Function(List<ItemDropperItem<T>>) |
required | Called when the selection changes |
popupItemBuilder |
Widget Function(...)? |
null |
Optional custom builder for popup items |
width |
double |
required | The width of the dropdown field |
enabled |
bool |
true |
Whether the dropdown is enabled |
hintText |
String? |
null |
Hint/placeholder text for input field |
maxSelected |
int? |
null |
Maximum number of items selectable in multi-select dropdown |
onAddItem |
ItemDropperItem<T>? Function(String)? |
null |
Callback for adding new items based on search text entered by user |
onDeleteItem |
Function(ItemDropperItem<T>)? |
null |
Callback for deleting items |
maxDropdownHeight |
double |
200.0 |
Maximum dropdown popup height |
showScrollbar |
bool |
true |
Keep the popup scrollbar thumb visible |
scrollbarThickness |
double |
6.0 |
Popup vertical scrollbar thickness |
itemHeight |
double? |
null |
Height for popup dropdown items |
popupTextStyle |
TextStyle? |
null |
Text style for popup dropdown items |
popupGroupHeaderStyle |
TextStyle? |
null |
Text style for group headers in popup |
fieldTextStyle |
TextStyle? |
null |
Text style for input/search field and chips |
selectedChipDecoration |
BoxDecoration? |
null |
Custom BoxDecoration for selected chips |
fieldDecoration |
BoxDecoration? |
null |
Optional BoxDecoration for field container |
elevation |
double? |
null |
Popup shadow elevation (null uses 4.0) |
inputKey |
GlobalKey? |
null |
Key for programmatic access to widget |
showDropdownPositionIcon |
bool |
true |
Show the dropdown position arrow (down/up) |
showDeleteAllIcon |
bool |
true |
Show the clear (X) icon (clears search/all selections) |
localizations |
ItemDropperLocalizations? |
null |
Localization strings for user-facing text (optional) |
dependencies |
MultiItemDropperDependencies<T>? |
null |
Dependency factory hooks for tests and advanced integrations |
ItemDropperItem Properties
| Property | Type | Default | Description |
|---|---|---|---|
value |
T |
required | Value associated with the item |
label |
String |
required | Display text |
isGroupHeader |
bool |
false |
Whether item is a group header |
isEnabled |
bool |
true |
Whether item can be selected |
isDeletable |
bool |
false |
Whether item can be deleted |
isAddItem |
bool |
false |
Reserved sentinel for internally generated add-item rows |
ItemDropperItem uses value equality across all six properties, so equivalent items behave
consistently in comparisons, sets, and map keys.
Shared Widget Contract
SingleItemDropper<T> and MultiItemDropper<T> implement ItemDropperBase<T>. Use this shared
type when application code needs to accept either widget while reading their common configuration.
Dependency Hooks
SingleItemDropperDependencies<T> and MultiItemDropperDependencies<T> provide optional factory
hooks for substituting managers and controllers in focused tests or advanced integrations. Normal
application usage does not need to pass dependencies.
π Full Documentation
- Complete Working Examples - See comprehensive examples in the demo app
- API Documentation - Auto-generated API reference
- GitHub Repository - View source code and contribute
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
π Issues
Found a bug or have a feature request? Please open an issue on GitHub.
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Acknowledgments
Built with β€οΈ using Flutter. Designed for developers who need powerful, accessible dropdown components.
π Changelog
See CHANGELOG.md for a list of changes in each version.
β Show Your Support
If you find this package useful, please give it a β on GitHub!
Keywords: flutter, dropdown, select, multi-select, autocomplete, searchable, accessible, keyboard navigation, chips