CriteriaSelector

A highly customizable Flutter selector library. Supports SelectorBox, DropdownSelectorBar, DropdownSelectorButton, dialog, and bottom-sheet selectors.

Features

Entry points

  • SelectorBox, DropdownSelectorBar, DropdownSelectorButton, showSelector (dialog), and showModalBottomSelector (bottom sheet) — all built on the same SelectorDelegate, 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.custom accepts min/max input, and immediate entries apply without the action bar.

Customization

  • skeletonBuilder & errorBuilder for loading/error states.
  • Theming via SelectorThemeData and the DropdownSelectorBarTheme / DropdownSelectorButtonTheme extensions.
  • 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));

CascadingSelectorDelegate

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),
);

GridSelectorDelegate

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));

ListSelectorDelegate

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,
  ),
);

FlattenSelectorDelegate

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.

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
  },
);

DropdownSelectorBar

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),
);

DropdownSelectorButton

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
}

showSelector

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
}

showModalBottomSelector

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_selector package.