anchored_dropdown_field 0.3.1
anchored_dropdown_field: ^0.3.1 copied to clipboard
Animated, anchored Flutter dropdown fields with forms, single and multiple selection, local filtering, async search, and customizable menus.
anchored_dropdown_field #
An animated and form-aware Flutter dropdown built on MenuAnchor. It supports
single and multiple selection, local filtering, debounced asynchronous search,
validation, and extensive presentation customization.
Features #
- Controlled and
FormFieldAPIs for single and multiple selection. - Menus that match the field width and grow only as tall as their content.
- Keyboard-aware panel sizing and placement, with the selected option revealed when opening.
- In-field search across labels, descriptions, and semantic labels.
- Debounced asynchronous search with stale-response protection.
- Loading, error, retry, empty, header, footer, and separator customization.
- Custom item, selected-value, indicator, icon, and state styles.
- External focus, menu, and search controllers when advanced control is needed.
- Material theming, keyboard focus, hover behavior, and accessibility semantics.
- Configurable open, close, and indicator animations.
Installation #
Add the package to pubspec.yaml:
dependencies:
anchored_dropdown_field: ^0.3.0
Basic usage #
import 'package:anchored_dropdown_field/anchored_dropdown_field.dart';
DropdownFormField<String>(
value: selectedRole,
decoration: const InputDecoration(labelText: 'Role'),
items: const [
DropdownItem(value: 'design', label: 'Designer'),
DropdownItem(value: 'engineering', label: 'Engineer'),
],
onChanged: (value) => setState(() => selectedRole = value),
validator: (value) => value == null ? 'Select a role' : null,
)
DropdownField provides the same presentation without form validation.
Local search #
Enable default local filtering with one optional configuration object:
Opening a searchable dropdown turns the field itself into the search input. The options panel contains results, not a second text field. Closing the menu restores the selected value or values; typing and clearing a query never change the selection.
DropdownFormField<Country>(
decoration: const InputDecoration(
labelText: 'Country',
hintText: 'Find a country',
),
items: countries
.map(
(country) => DropdownItem(
value: country,
label: country.name,
description: country.region,
),
)
.toList(),
search: const DropdownSearchConfig<Country>(),
onChanged: (country) => setState(() => selectedCountry = country),
)
Use filter when matching depends on fields that are not displayed:
search: DropdownSearchConfig<Country>(
filter: (item, query) {
final normalized = query.toLowerCase();
return item.label.toLowerCase().contains(normalized) ||
item.value.isoCode.toLowerCase().contains(normalized);
},
),
Asynchronous search #
Supplying onSearch changes the search source from local items to asynchronous
results. Requests are debounced and late responses from older queries are
ignored automatically.
DropdownFormField<Person>(
value: selectedPerson,
items: const [],
search: DropdownSearchConfig<Person>(
minimumQueryLength: 2,
debounceDuration: const Duration(milliseconds: 350),
onSearch: (query) async {
final people = await directory.search(query);
return people.map(
(person) => DropdownItem(
value: person,
label: person.name,
description: person.email,
),
);
},
errorText: 'Could not load people',
retryText: 'Try again',
),
onChanged: (person) => setState(() => selectedPerson = person),
)
The search controller, focus node, loading widget, error widget, query callbacks, minimum query length, and result retention are configurable. The controller contains only the query, never the selected label.
Use search.textCapitalization to request capitalization from the search
keyboard, for example TextCapitalization.sentences. The default remains
TextCapitalization.none; this does not transform typed or pasted queries.
The anchor retains its field decoration, including labels, helper text, errors,
icons, and borders, while searching. Configure the placeholder with
decoration.hintText and decoration.hintStyle on the dropdown itself. Search
does not have a separate decoration. The deprecated search.padding is ignored;
use the field's decoration.contentPadding instead.
Search uses search.focusNode when supplied, otherwise the field's
focusNode, or an internally owned node. With the default autofocus: true,
opening the menu focuses the input. Menu hover does not steal that focus.
Escape closes the menu, and Arrow Down moves focus to the first enabled option.
When the keyboard appears or disappears, the panel recalculates its available
height and moves above the field when necessary.
Multiple selection #
MultiDropdownFormField<String>(
values: selectedSkills,
maxSelections: 4,
decoration: const InputDecoration(labelText: 'Skills'),
items: const [
DropdownItem(value: 'dart', label: 'Dart'),
DropdownItem(value: 'flutter', label: 'Flutter'),
DropdownItem(value: 'design', label: 'Product design'),
],
search: const DropdownSearchConfig<String>(),
onChanged: (values) => setState(() => selectedSkills = values),
)
Multiple-selection menus remain open by default. Set closeOnSelection when a
different interaction is preferred. Search text and keyboard focus are retained
while selecting multiple options. By default, closing clears the query; set
clearQueryOnClose: false to retain it for the next opening.
Opening reveals the first selected option in the current result order. This
also works when asynchronous results arrive, without taking focus from search.
Styling #
DropdownFieldStyle controls the menu, options, dimensions, overlay behavior,
and animations. Per-item styles and builders can override global defaults.
Outside taps only dismiss the panel by default, without activating the tapped
control. Set consumeOutsideTap: false explicitly to allow tap-through.
DropdownFormField<String>(
style: const DropdownFieldStyle(
menuMaxHeight: 320,
indicatorAnimationDuration: Duration(milliseconds: 180),
selectedItemStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Color(0xFFE8F2FF)),
),
),
itemBuilder: (context, item, state) => Text(item.label),
selectionIndicatorBuilder: (context, item, state) =>
state.selected ? const Icon(Icons.check_rounded) : null,
items: const [
DropdownItem(value: 'one', label: 'One'),
DropdownItem(value: 'two', label: 'Two'),
],
onChanged: (value) {},
)
See the included example for local search, asynchronous search, validation, and multiple selection in one application.