flutter_bottom_sheet_pickers
Customizable Flutter bottom sheet pickers for single selection, multiple selection, searchable lists, paged lazy loading, up to three-level cascade selection, dates, date ranges, and year-month ranges.
Use it for form fields, filters, region pickers, store pickers, organization trees, and remote option lists that should feel native inside a Flutter bottom sheet. The package depends only on the Flutter SDK and exposes chainable builders, so most pickers can be opened with one short expression.
Features
- Single and multiple bottom sheet pickers
- Searchable local option lists
- Paged lazy loading for remote option lists
- Single and multiple cascade pickers with up to three levels
- Cascade options from
CascadeOption, map-like lists, or adjacency maps - Disabled options, optional empty confirmation, and custom option rows
- Optional checkboxes for multiple selection
- Date, date range, year-month, and year-month range pickers
- Calendar helper labels for Gregorian, lunar, Buddhist, Tibetan, Islamic, Yi, and Hebrew calendars
- Month and year shortcut navigation that automatically respects the allowed date range
- Built-in cancel, reset, and confirm actions
- Configurable primary color and action button border radius
- Built-in labels for English, simplified Chinese, traditional Chinese, Thai, Burmese, Brazilian Portuguese, Canadian French, Italian, and Spanish
Language: English | 中文
Platform Support
| Android | iOS | MacOS | Web | Linux | Windows |
|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Requirements
- Flutter >=3.13.0 <4.0.0
- Dart >=3.1.0 <4.0.0
Installation
Add the package to your pubspec.yaml:
dependencies:
flutter_bottom_sheet_pickers: ^0.1.1
Import it:
import 'package:flutter_bottom_sheet_pickers/flutter_bottom_sheet_pickers.dart';
API overview
| Entry point | Use case | Return value |
|---|---|---|
BottomSheetPickers.single<T>(context) |
Single selection, searchable single selection, lazy single selection | Future<T?> |
BottomSheetPickers.multiple<T>(context) |
Multiple selection, searchable multiple selection, lazy multiple selection | Future<List<T>?> |
BottomSheetPickers.cascade(context) |
Up to three-level cascade single selection | Future<CascadeSelection?> |
BottomSheetPickers.cascade(context).multiple() |
Up to three-level cascade multiple selection | Future<List<CascadeSelection>?> |
BottomSheetPickers.calendar(context) |
Date selection | Future<DateTime?> |
BottomSheetPickers.dateRange(context) |
Date range selection | Future<DateTimeRange?> |
BottomSheetPickers.yearMonth(context) |
Year-month or year-month range selection | Future<YearMonth?> / Future<YearMonthRange?> |
BottomSheetPickers.setLocalizations(...) |
App-wide picker labels | void |
BottomPickerConfig(...) |
Local picker labels for one widget subtree | Widget |
Getting started
Single Picker
final String? selected = await BottomSheetPickers.single<String>(
context,
title: "Choose a fruit",
).options(
["Apple", "Orange", "Banana"],
initialValue: "Apple",
).show();
Confirm on Option Tap
final String? selected = await BottomSheetPickers.single<String>(
context,
title: "Choose a fruit",
).options(
["Apple", "Orange", "Banana"],
).confirmOnTap()
.show();
Custom Sheet Height
final String? selected = await BottomSheetPickers.single<String>(
context,
title: "Choose a fruit",
).height(360)
.options(["Apple", "Orange", "Banana"])
.show();
Multiple Picker
final List<String>? selected = await BottomSheetPickers.multiple<String>(
context,
title: "Choose tags",
).options(
["New", "Popular", "Recommended"],
initialValue: ["New"],
).show();
Multiple Picker with Checkboxes
final List<String>? selected = await BottomSheetPickers.multiple<String>(
context,
title: "Choose tags",
).options(
["New", "Popular", "Recommended"],
initialValue: ["New"],
).checkbox()
.show();
Search
final String? selected = await BottomSheetPickers.single<String>(
context,
title: "Choose a city",
).options(cities)
.searchSupported(placeholder: "Search city")
.show();
Lazy Loading
final String? selected = await BottomSheetPickers.single<String>(
context,
title: "Choose a store",
).lazyLoad(
parameters: {"country": "TH"},
lazyRequestFuture: (params) async {
final pageIndex = params["page_index"] as int;
final pageSize = params["page_size"] as int;
final keyword = params["keyword"] as String?;
return loadStores(pageIndex: pageIndex, pageSize: pageSize, keyword: keyword);
},
).show();
The lazy loader receives page_index, page_size, and keyword in the parameter map and should return the current page as List<T>. When search is enabled, keyword contains the active search text.
Cascade Picker
final CascadeSelection? selected = await BottomSheetPickers.cascade(
context,
title: "Choose location",
).options(
[
CascadeOption(
id: "province_a",
label: "Province A",
children: [
CascadeOption(
id: "city_a",
label: "City A",
children: [
CascadeOption(id: "town_a", label: "Town A"),
],
),
],
),
],
).initialValue(CascadeSelection.byIds("province_a", "city_a", "town_a"))
.cascadeAllItemSupported()
.show();
Multiple Cascade Picker
final List<CascadeSelection>? selected = await BottomSheetPickers.cascade(
context,
title: "Choose locations",
).options(options)
.multiple()
.initialValues([
CascadeSelection.byIds("province_a", "city_a", "town_a"),
])
.cascadeAllItemSupported(allItemLabel: "All")
.show();
Map-like Cascade Data
final options = [
{
"id": 1,
"label": "Province A",
"value": "province-a",
"children": [
{
"id": 11,
"label": "City A",
"children": [
{"id": 111, "label": "Town A"}
]
}
]
}
];
The parser reads the option label from label, value, name, then id.
You can also pass an adjacency map. Each key is a parent id, and its list contains the child nodes:
final adjacencyOptions = {
null: [
{"id": "province_a", "label": "Province A"}
],
"province_a": [
{"id": "city_a", "label": "City A"}
],
"city_a": [
{"id": "town_a", "label": "Town A"}
],
};
Use CascadeSelection.byIds(...) for initial values. The picker resolves those ids against the option tree and returns a CascadeSelection containing the matched CascadeOption objects.
Disabled options and empty confirmation
final selected = await BottomSheetPickers.multiple<String>(
context,
title: "Choose tags",
).options(
tags,
disabledValues: ["Archived"],
).allowNoSelection()
.show();
disabledValues are compared with the option value. For cascade pickers, pass the option ids or values that should be disabled. allowNoSelection() lets users confirm without selecting an item.
Date Picker
final DateTime? selectedDate = await BottomSheetPickers.calendar(
context,
title: "Choose date",
initialDate: DateTime(2026, 7, 7),
firstDate: DateTime(2026, 1, 1),
lastDate: DateTime(2026, 12, 31),
).show();
firstDate and lastDate are inclusive bounds. The previous month, next month, previous year, and next year buttons are shown only when the target month is inside those bounds.
Date Range Picker
final DateTimeRange? range = await BottomSheetPickers.dateRange(
context,
title: "Choose period",
initialDateRange: DateTimeRange(
start: DateTime(2026, 7, 1),
end: DateTime(2026, 7, 7),
),
firstDate: DateTime(2026, 1, 1),
lastDate: DateTime(2026, 12, 31),
).show();
Calendar Helper Labels
final DateTime? selectedDate = await BottomSheetPickers.calendar(
context,
initialDate: DateTime(2026, 7, 7),
).calendarType(CalendarType.yi)
.show();
The header and returned value remain Gregorian. When a CalendarType is specified, day cells show small helper labels for that calendar.
Year-Month Picker
final YearMonth? selectedMonth = await BottomSheetPickers.yearMonth(
context,
title: "Choose month",
initialYearMonth: const YearMonth(2026, 7),
firstYearMonth: const YearMonth(2020, 1),
lastYearMonth: const YearMonth(2030, 12),
).show();
Year-Month Range Picker
final YearMonthRange? range = await BottomSheetPickers.yearMonth(
context,
title: "Choose month range",
firstYearMonth: const YearMonth(2020, 1),
lastYearMonth: const YearMonth(2030, 12),
isRange: true,
).show();
When no initial range is provided, the start defaults to the current year-month and the end is empty until the user selects one. End candidates start from the selected start year-month.
Return Values
- Confirm in a single picker returns
T?. - Confirm in a multiple picker returns
List<T>?. - Confirm in a single cascade picker returns
CascadeSelection?. - Confirm in a multiple cascade picker returns
List<CascadeSelection>?. - Confirm in a date picker returns
DateTime?. - Confirm in a date range picker returns
DateTimeRange?. - Confirm in a year-month picker returns
YearMonth?. - Confirm in a year-month range picker returns
YearMonthRange?. - Cancel, tapping outside the sheet, and system back return
null. - Reset returns an empty list for multiple pickers and a reset selection for single cascade pickers.
Theme
BottomPickerTheme derives button background, button border, checked color, selected option background, and disabled button background from primaryColor.
final selected = await BottomSheetPickers.single<String>(
context,
title: "Choose a fruit",
themeData: const BottomPickerTheme(
primaryColor: Color(0xFF1677FF),
buttonBorderRadius: BorderRadius.all(Radius.circular(12)),
),
).options(fruits).show();
Localization
The package does not require a localization delegate. By default, labels are resolved from the current Flutter locale, then the platform locale, then English.
Built-in labels are available through:
BottomPickerLocalizations.en
BottomPickerLocalizations.zh
BottomPickerLocalizations.zhHant
BottomPickerLocalizations.th
BottomPickerLocalizations.my
BottomPickerLocalizations.ptBR
BottomPickerLocalizations.frCA
BottomPickerLocalizations.it
BottomPickerLocalizations.es
For app-level configuration:
BottomSheetPickers.setLocalizations(
localizations: BottomPickerLocalizations.byLocale(currentLocale),
);
For apps that use their own localization extension:
BottomSheetPickers.setLocalizations(
builder: (context) => BottomPickerLocalizations(
cancel: context.i18n("cancel"),
reset: context.i18n("reset"),
confirm: context.i18n("confirm"),
),
);
Unset labels fall back to the built-in labels for the active locale.
Use BottomPickerConfig when only one subtree needs a local override:
BottomPickerConfig(
localizations: BottomPickerLocalizations.zh,
child: PageContent(),
)
Additional information
- The runnable example app lives in
example/. - Before publishing, run
flutter analyze,flutter test, andflutter pub publish --dry-run. - Feel free to file an issue if you have any problem or feature request.
Libraries
- flutter_bottom_sheet_pickers
- Customizable bottom sheet pickers for Flutter.