fl_select 0.8.0
fl_select: ^0.8.0 copied to clipboard
A customizable Flutter select widget for building filter bars, cascading menus, and pickers with single/multiple selection, async loading, search filtering, theming, and i18n.
A customizable Flutter select widget for building filter bars, cascading menus, and pickers with single/multiple selection, async loading, search filtering, theming, and i18n.

Agent Skills #
This repo bundles an Agent Skill so AI coding agents (Claude Code, Cursor, Codex, Windsurf, GitHub Copilot, etc.) use fl_select correctly — accurate APIs, no hallucinated parameters.
Install it with:
npx skills add amlzq/fl_select
Then just ask your agent to build a filter bar or select UI with fl_select. The skill lives in skills/fl-select.
Features #
Two layers work together: entry points decide where the select appears, and delegates decide how entries are laid out — any delegate plugs into any entry point.
- Entry points — five ways to show a select:
SelectView,PopupSelectBar,PopupSelectButton,showSelect,showModalBottomSelect. - Delegates — four navigation styles:
CascadingSelectDelegate,GridSelectDelegate,ListSelectDelegate,FlattenSelectDelegate. In all but the cascading one, each category's children are laid out bycategory.layout— list / grid / chips / range slider / counter. - Single & multiple selection via
SelectionMode(per category or as a delegate fallback). - Async data loading through
entriesLoader. - Search filtering: set
searchEnabledon any delegate and aSelectSearchBarfilters entries as you type (debounced, with a customizable predicate and theme). - Flexible entries: the "Any" entry clears a category,
SelectRangeEntry.customtakes user min/max input, and animmediateentry applies on tap without the action bar. skeletonBuilder&errorBuilderfor loading and error states.- Theming via
SelectThemeDataand thePopupSelectBarTheme/PopupSelectButtonThemeextensions. - Built-in i18n in 10 languages via
SelectLocalizationsDelegate.
Getting started #
Install
flutter pub add fl_select
Import
import 'package:fl_select/fl_select.dart';
Usage #
Delegates
A delegate controls both data loading and how the body is rendered, and any delegate works with every entry point above.
The built-in delegates are:
| Delegate | Description | Preview |
|---|---|---|
CascadingSelectDelegate |
A tree select: categories on the left, a cascading list on the right. | ![]() |
GridSelectDelegate |
A grid layout (crossAxisCount is required; children follow category.layout, default grid). |
![]() |
ListSelectDelegate |
A single-column list (use .name(...) leaves for a flat list; children follow category.layout, default list). |
![]() |
FlattenSelectDelegate |
Renders children by category.layout (default chips) under a category sidebar synced to the scrolling column. Best with SelectionMode.multiple and an "Any" entry. |
![]() |
SelectEntry
Entries form a tree. SelectCategoryEntry is the root (a category) and SelectChildEntry is any non-root node, identified by its parentId.
| Entry | Purpose |
|---|---|
SelectCategoryEntry |
Root node. Holds children and the selectionMode for them. |
SelectTextEntry |
A plain text leaf. Use .any(...) for the "Any" (clear) entry. .name(...) creates a parentless leaf for flat lists. |
SelectRangeEntry<N, E> |
A numeric range leaf (min/max). Use .any(...) for "Any" and .custom(...) for a user-input range. SelectIntEntry<E> is a handy alias for SelectRangeEntry<int, E>. |
Selection is controlled by SelectionMode (single by default, or multiple), set on a SelectCategoryEntry (per category) or on the delegate (fallback). In multiple-selection mode, an entry with immediate: true applies on tap and skips the action bar.
Entries load asynchronously via entriesLoader, which returns a Future<SelectEntries> where SelectEntries is Set<SelectEntry>.
// A category with single-selection children
SelectCategoryEntry(
id: 'price',
name: 'Price',
children: {
SelectIntEntry.any(parentId: 'price', name: 'Any'),
SelectIntEntry(parentId: 'price', id: '0-100', name: '0-100', min: 0, max: 100),
SelectIntEntry.custom(parentId: 'price', name: 'Custom'),
},
);
// A multi-selection category
SelectCategoryEntry(
id: 'more',
name: 'More',
selectionMode: SelectionMode.multiple,
children: {
SelectTextEntry.any(parentId: 'more', name: 'Any'),
SelectTextEntry(parentId: 'more', id: 'near_subway', name: 'Near subway'),
},
);
// Parentless leaves for a flat list
SelectTextEntry.name(id: 'default', name: 'Default');
SelectView
SelectView embeds a select directly in a page or dialog body. Pass any delegate — it controls both loading and rendering.
SelectView(
delegate: CascadingSelectDelegate(entriesLoader: _fetchNeighborhood),
onChanged: (selected) {
// selected is the SelectEntries when the selection changes
},
);
PopupSelectBar
A tab bar (PreferredSizeWidget) that opens an overlay select when a tab is tapped. Provide tabs for the bar and a matching selectDelegates list (one per tab). Results arrive via onChanged / onApplied / onReset.
PopupSelectBar(
tabs: const [
PopupTab(label: 'Neighborhood'),
PopupTab(label: 'Price'),
PopupTab(label: 'Rooms'),
PopupTab(label: 'More'),
PopupTab(label: 'Sort'),
],
selectDelegates: [
CascadingSelectDelegate(entriesLoader: _fetchNeighborhood),
GridSelectDelegate(crossAxisCount: 3, entriesLoader: _fetchPrice),
GridSelectDelegate(crossAxisCount: 3, entriesLoader: _fetchRooms),
FlattenSelectDelegate(entriesLoader: _fetchMore),
ListSelectDelegate(entriesLoader: _fetchSort),
],
onApplied: (tabData, selected) {
// tabData is the PopupTabData; selected is the SelectEntries
},
);

PopupSelectButton
A single-trigger alternative to PopupSelectBar — opens a select overlay on tap, like PopupMenuButton. It takes one selectDelegate and a label/child. Three variants: filled (default), .elevated(...), and .outlined(...).
PopupSelectButton(
label: 'Neighborhood',
selectDelegate: GridSelectDelegate(crossAxisCount: 3, entriesLoader: _fetchNeighborhood),
onApplied: (selected) { /* ... */ },
);
PopupSelectButton.elevated(
label: 'Price',
selectDelegate: GridSelectDelegate(crossAxisCount: 3, entriesLoader: _fetchPrice),
);
PopupSelectButton.outlined(
label: 'Rooms',
icon: const Icon(Icons.filter_alt_outlined),
selectDelegate: GridSelectDelegate(crossAxisCount: 3, entriesLoader: _fetchRooms),
);

showSelect
Shows a select in a modal dialog. Returns the selected SelectEntries when applied, or null when dismissed. In single-selection mode, tapping an item applies immediately; in multi-selection mode, "Apply" in the action bar confirms.
final SelectEntries? selected = await showSelect(
context: context,
delegate: FlattenSelectDelegate(entriesLoader: _fetchRooms),
title: const Text('Rooms'),
);
if (selected != null) {
// a selection was applied
}

showModalBottomSelect
Shows a select in a modal bottom sheet built on Flutter's showModalBottomSheet. Same interaction as showSelect. Standard sheet parameters (isScrollControlled, isDismissible, enableDrag, showDragHandle, constraints, etc.) are forwarded.
final SelectEntries? selected = await showModalBottomSelect(
context: context,
delegate: ListSelectDelegate(
selectionMode: SelectionMode.multiple,
entriesLoader: _fetchMore,
),
title: const Text('More'),
);
if (selected != null) {
// a selection was applied
}

Search
Set searchEnabled: true on any delegate to render a SelectSearchBar above the body. Typing filters the displayed entries (debounced 300 ms by default) while preserving the layout and selection state — canceling the search restores the original entries.
CascadingSelectDelegate(
entriesLoader: _fetchNeighborhood,
searchEnabled: true,
searchHintText: 'Search',
searchDebounceDuration: const Duration(milliseconds: 300),
// searchPredicate: (entry, query) => ..., // defaults to a case-insensitive
// // substring match on SelectEntry.name
);
The default predicate (defaultSelectSearchPredicate) matches SelectEntry.name case-insensitively; provide a custom searchPredicate to match id, extra, or any other field. Style the bar via searchBarTheme (SelectSearchBarTheme) on the delegate, or globally through SelectThemeData.
Serializing selections
Selections arrive as a SelectEntries tree. Two extensions turn that tree into URL query parameters — each category contributes key/value pairs keyed by its own id with the deepest selected leaf ids as values; an "Any" leaf resolves to its parent id; a custom SelectRangeEntry formats as min-max:
final selected = await showSelect(context: context, delegate: ...);
// Map<String, List<String>>, mirroring Uri.queryParametersAll
final map = selected?.toQueryMap(); // {price: [0-100], more: [near_subway]}
// Or a ready-made query string
selected?.toQueryParameters(); // price=0-100&more=near_subway
// Multi-value layouts via SelectArrayFormat
selected?.toQueryParameters(arrayFormat: SelectArrayFormat.brackets); // more[]=a&more[]=b
selected?.toQueryParameters(arrayFormat: SelectArrayFormat.comma); // more=a,b
selected?.toQueryParameters(arrayFormat: SelectArrayFormat.indices); // more[0]=a
selected?.toQueryParameters(
arrayFormat: SelectArrayFormat.delimited,
delimiter: '|',
); // more=a|b (covers OpenAPI pipeDelimited / spaceDelimited)
Values are percent-encoded by default; pass encode: false when the caller handles encoding.
Theming
Per instance — delegates carry the styling: set selectedColor / onSelectedColor (or any finer-grained *Theme field) directly on a delegate. PopupSelectBar also accepts a single selectTheme that overrides the styling of every tab's delegate:
ListSelectDelegate(
entriesLoader: _fetchSort,
selectedColor: Theme.of(context).colorScheme.primary,
onSelectedColor: Theme.of(context).colorScheme.onPrimary,
);
PopupSelectBar(
tabs: ...,
selectDelegates: ...,
selectTheme: SelectThemeData(Theme.of(context)),
);
Globally — register PopupSelectBarTheme and PopupSelectButtonTheme as ThemeData extensions so every bar/button picks them up automatically:
MaterialApp(
theme: ThemeData(
extensions: [
PopupSelectBarTheme(
height: 48,
labelColor: Colors.blue,
selectTheme: SelectThemeData(ThemeData.light()),
),
PopupSelectButtonTheme(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
],
),
);
Internationalization
Add SelectLocalizationsDelegate() to your MaterialApp. It ships translations for de, en, es, fr, id, ja, ko, pt, vi, and zh (Hans/Hant), localizing the "Apply" / "Reset" / "Multiple" labels automatically.
const localizationsDelegates = <LocalizationsDelegate>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
SelectLocalizationsDelegate(),
];
const supportedLocales = SelectLocalizationsDelegate.supportedLocales;
MaterialApp(
localizationsDelegates: localizationsDelegates,
supportedLocales: supportedLocales,
home: const HomePage(),
);
To override the labels for a single delegate, set applyText / resetText on it directly.



