CriteriaSelector
A highly customizable Flutter selector library. Supports SelectorBox, DropdownSelectorBar, DropdownSelectorButton, dialog, and bottom-sheet selectors.
Features
Entry points
SelectorBox,DropdownSelectorBar,DropdownSelectorButton,showSelector(dialog), andshowModalBottomSelector(bottom sheet) — all built on the sameSelectorDelegate, so any layout works on any surface.
Layouts (via SelectorDelegate)
CascadingSelectorDelegate,GridSelectorDelegate,ListSelectorDelegate,FlattenSelectorDelegate.
Selection & data
- Single & multiple selection via
SelectionMode(per category or as a delegate fallback). - Async data loading through
entriesLoader. - Flexible entries: an "Any" entry clears a category,
SelectorRangeEntry.customaccepts min/max input, andimmediateentries apply without the action bar.
Customization
skeletonBuilder&errorBuilderfor loading/error states.- Theming via
SelectorThemeDataand theDropdownSelectorBarTheme/DropdownSelectorButtonThemeextensions. - Built-in i18n (10 languages) via
CriteriaSelectorLocalizationsDelegate.
Getting started
Install
flutter pub add criteria_selector
Import
import 'package:criteria_selector/criteria_selector.dart';
Usage
Delegates
The building blocks below are shared by every entry point. Each delegate decides how entries are loaded and how the selector body is rendered.
Common concepts
Entries form a tree. SelectorCategoryEntry is the root (a category) and SelectorChildEntry is any non-root node, identified by its parentId.
| Entry | Purpose |
|---|---|
SelectorCategoryEntry |
Root node. Holds children and the selectionMode for them. |
SelectorTextEntry |
A plain text leaf. Use .any(...) for the "Any" (clear) entry. .name(...) creates a parentless leaf for flat lists. |
SelectorRangeEntry<N, E> |
A numeric range leaf (min/max). Use .any(...) for "Any" and .custom(...) for a user-input range. SelectorIntEntry<E> is a handy alias for SelectorRangeEntry<int, E>. |
Selection is controlled by SelectionMode (single by default, or multiple), set on a SelectorCategoryEntry (per category) or on the delegate (fallback).
Entries load asynchronously via entriesLoader, which returns a Future<SelectorEntries> where SelectorEntries is Set<SelectorEntry>.
// A category with single-selection children
SelectorCategoryEntry(
id: 'price',
name: 'Price',
children: {
SelectorRangeEntry<int, void>.any(parentId: 'price', name: 'Any'),
SelectorRangeEntry<int, void>(parentId: 'price', id: '0-100', name: '0-100', min: 0, max: 100),
SelectorRangeEntry<int, void>.custom(parentId: 'price', name: 'Custom'),
},
);
// A multi-selection category
SelectorCategoryEntry(
id: 'more',
name: 'More',
selectionMode: SelectionMode.multiple,
children: {
SelectorTextEntry.any(parentId: 'more', name: 'Any'),
SelectorTextEntry(parentId: 'more', id: 'near_subway', name: 'Near subway'),
},
);
// Parentless leaves for a flat list
SelectorTextEntry.name(id: 'default', name: 'Default');
Every delegate below is rendered by any entry point — SelectorBox (shown next) is the simplest.
CascadingSelectorDelegate
A tree selector: categories on the left, a cascading list on the right.
Future<SelectorEntries> _fetchRegion() async {
return {
SelectorCategoryEntry(
id: 'region',
name: 'Region',
children: {
SelectorTextEntry.any(parentId: 'region', name: 'Any'),
SelectorTextEntry(
parentId: 'region',
id: 'bj',
name: 'Beijing',
children: {
SelectorTextEntry(parentId: 'bj', id: 'cy', name: 'Chaoyang'),
SelectorTextEntry(parentId: 'bj', id: 'hd', name: 'Haidian'),
},
),
},
),
};
}
SelectorBox(delegate: CascadingSelectorDelegate(entriesLoader: _fetchRegion));

GridSelectorDelegate
A grid layout. crossAxisCount is required.
Future<SelectorEntries> _fetchPrice() async {
return {
SelectorCategoryEntry(
id: 'price',
name: 'Price',
children: {
SelectorRangeEntry<int, void>.any(parentId: 'price', name: 'Any'),
SelectorRangeEntry<int, void>(parentId: 'price', id: '0-100', name: '0-100', min: 0, max: 100),
SelectorRangeEntry<int, void>(parentId: 'price', id: '100-500', name: '100-500', min: 100, max: 500),
SelectorRangeEntry<int, void>.custom(parentId: 'price', name: 'Custom'),
},
),
};
}
SelectorBox(
delegate: GridSelectorDelegate(crossAxisCount: 3, entriesLoader: _fetchPrice),
);

ListSelectorDelegate
A single-column list. Use parentless SelectorTextEntry.name(...) leaves for a flat list.
Future<SelectorEntries> _fetchSort() async {
return {
SelectorTextEntry.name(id: 'default', name: 'Default'),
SelectorTextEntry.name(id: 'price_low', name: 'Price: low to high'),
SelectorTextEntry.name(id: 'price_high', name: 'Price: high to low'),
};
}
SelectorBox(delegate: ListSelectorDelegate(entriesLoader: _fetchSort));

FlattenSelectorDelegate
Renders children in a grid while keeping the category hierarchy. Best with SelectionMode.multiple and an "Any" entry.
Future<SelectorEntries> _fetchMore() async {
return {
SelectorCategoryEntry(
id: 'more',
name: 'More',
selectionMode: SelectionMode.multiple,
children: {
SelectorTextEntry.any(parentId: 'more', name: 'Any'),
SelectorTextEntry(parentId: 'more', id: 'near_subway', name: 'Near subway'),
SelectorTextEntry(parentId: 'more', id: 'pet_friendly', name: 'Pet friendly'),
},
),
};
}
SelectorBox(
delegate: FlattenSelectorDelegate(
crossAxisCount: 3,
selectionMode: SelectionMode.multiple,
entriesLoader: _fetchMore,
),
);

SelectorBox
SelectorBox embeds a selector directly in a page or dialog body. Pass any delegate from the Delegates section above — it controls both loading and rendering.
DropdownSelectorBar
A tab bar (PreferredSizeWidget) that opens an overlay selector when a tab is tapped. Provide tabs for the bar and a matching selectorDelegates list (one per tab). Results arrive via onChanged / onApplied / onReset.
DropdownSelectorBar(
tabs: const [
DropdownTab(label: 'Region'),
DropdownTab(label: 'Price'),
DropdownTab(label: 'Sort'),
],
selectorDelegates: [
CascadingSelectorDelegate(entriesLoader: _fetchRegion),
GridSelectorDelegate(crossAxisCount: 3, entriesLoader: _fetchPrice),
ListSelectorDelegate(entriesLoader: _fetchSort),
],
onApplied: (result) {
// result is a DropdownSelectorResult; result.selected is the SelectorEntries
},
);

DropdownSelectorButton
A single-trigger alternative to DropdownSelectorBar — opens a selector overlay on tap, like PopupMenuButton. It takes one selectorDelegate and a label/child. Three variants: filled (default), .elevated(...), and .outlined(...).
DropdownSelectorButton(
label: 'Price',
selectorDelegate: GridSelectorDelegate(crossAxisCount: 3, entriesLoader: _fetchPrice),
onApplied: (result) { /* ... */ },
);
DropdownSelectorButton.elevated(
label: 'Price',
selectorDelegate: GridSelectorDelegate(crossAxisCount: 3, entriesLoader: _fetchPrice),
);
DropdownSelectorButton.outlined(
label: 'Price',
icon: const Icon(Icons.filter_alt_outlined),
selectorDelegate: GridSelectorDelegate(crossAxisCount: 3, entriesLoader: _fetchPrice),
);

showSelector
Shows a selector in a modal dialog. Returns the selected SelectorEntries 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 SelectorEntries? selected = await showSelector(
context: context,
delegate: ListSelectorDelegate(entriesLoader: _fetchSort),
title: const Text('Sort'),
);
if (selected != null) {
// a selection was applied
}

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

Theming
Per instance — pass selectorTheme to any selector entry point (SelectorBox, showSelector, showModalBottomSelector, DropdownSelectorBar, DropdownSelectorButton):
SelectorBox(
delegate: ListSelectorDelegate(entriesLoader: _fetchSort),
selectorTheme: SelectorThemeData(
Theme.of(context),
selectedColor: Theme.of(context).colorScheme.primary,
onSelectedColor: Theme.of(context).colorScheme.onPrimary,
),
);
Globally — register DropdownSelectorBarTheme and DropdownSelectorButtonTheme as ThemeData extensions so every bar/button picks them up automatically:
MaterialApp(
theme: ThemeData(
extensions: [
DropdownSelectorBarTheme(
height: 48,
labelColor: Colors.blue,
selectorTheme: SelectorThemeData(ThemeData.light()),
),
DropdownSelectorButtonTheme(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
],
),
);
Internationalization
Add CriteriaSelectorLocalizationsDelegate() to your MaterialApp. It ships translations for en, zh (Hans/Hant), es, pt, id, vi, fr, de, ja, and ko, localizing the "Apply" / "Reset" / "Multiple" labels automatically.
const localizationsDelegates = <LocalizationsDelegate>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
CriteriaSelectorLocalizationsDelegate(),
];
const supportedLocales = CriteriaSelectorLocalizationsDelegate.supportedLocales;
MaterialApp(
localizationsDelegates: localizationsDelegates,
supportedLocales: supportedLocales,
home: const HomePage(),
);
To override the labels for a single delegate, set applyText / resetText on it directly.
Libraries
- criteria_selector
- Public API for the
criteria_selectorpackage.