WSelect
A highly customizable, utility-first select component that supports single selection, multi-selection, searching, and remote data fetching.
- Basic Usage
- Constructor
- Props
- Layout Modes
- Event Handling
- Accessibility
- State Variants
- Styling Examples
- All Supported Classes
- Customizing Theme
- Related Documentation
WSelect(
value: _selected,
options: [
SelectOption(value: 'dart', label: 'Dart'),
SelectOption(value: 'flutter', label: 'Flutter'),
],
onChange: (value) => setState(() => _selected = value),
className: 'w-64 bg-white border border-gray-300 rounded-lg',
)
Basic Usage
The WSelect widget replaces the standard Material Dropdown with a utility-first approach. It manages its own open state while providing a controlled interface for selection.
WSelect(
placeholder: 'Choose a number',
options: List.generate(5, (i) => SelectOption(value: i, label: 'Option $i')),
onChange: (val) => print('Selected: $val'),
className: 'bg-white border p-3 rounded-md',
)
Constructor
const WSelect({
Key? key,
T? value,
ValueChanged? onChange,
bool isMulti = false,
List? values,
ValueChanged>? onMultiChange,
SelectedChipBuilder? selectedChipBuilder,
required List> options,
bool searchable = false,
Future>> Function(String)? onSearch,
String searchPlaceholder = 'Search...',
Future> Function(String)? onCreateOption,
CreateOptionBuilder? createOptionBuilder,
Future>> Function()? onLoadMore,
bool hasMore = false,
VoidCallback? onOpen,
String? className,
String? menuClassName,
String placeholder = 'Select an option',
bool disabled = false,
double? menuWidth,
double maxMenuHeight = 300,
Set? states,
SelectTriggerBuilder? triggerBuilder,
MultiSelectTriggerBuilder? multiTriggerBuilder,
SelectItemBuilder? itemBuilder,
EmptyStateBuilder? emptyBuilder,
LoadingBuilder? loadingBuilder,
})
Props
| Prop | Type | Default | Description |
|---|---|---|---|
className |
String? |
null |
Utility classes for the trigger container |
menuClassName |
String? |
null |
Utility classes for the dropdown menu |
options |
List |
Required | List of items to display |
value |
T? |
null |
Selected value in single-select mode |
onChange |
ValueChanged |
null |
Callback for single selection changes |
isMulti |
bool |
false |
Enables multi-select mode |
values |
List |
null |
Selected values in multi-select mode |
onMultiChange |
ValueChanged |
null |
Callback for multi-selection changes |
searchable |
bool |
false |
Shows a search input in the menu |
searchPlaceholder |
String |
'Search...' |
Placeholder text in the search input |
onSearch |
Future |
null |
Async remote search handler; returns filtered options for the query |
onCreateOption |
Future |
null |
Tagging handler; called when the user creates a new option from search input |
createOptionBuilder |
CreateOptionBuilder? |
null |
Custom builder for the "create new option" row in the menu |
onLoadMore |
Future |
null |
Pagination handler; called when the user scrolls past the current list |
hasMore |
bool |
false |
When true, indicates more pages are available for onLoadMore |
onOpen |
VoidCallback? |
null |
Called when the menu opens, after this widget has reset its own search and visible list. A paginating caller resets its cursor here; see below |
placeholder |
String |
'Select an option' |
Text shown when no value is selected |
disabled |
bool |
false |
Prevents interaction |
menuWidth |
double? |
null |
Fixed dropdown width; defaults to the trigger width |
maxMenuHeight |
double |
300 |
Maximum height of the dropdown list |
states |
Set |
null |
Custom states for dynamic styling |
triggerBuilder |
SelectTriggerBuilder |
null |
Custom trigger renderer for single-select |
multiTriggerBuilder |
MultiSelectTriggerBuilder |
null |
Custom trigger renderer for multi-select |
selectedChipBuilder |
SelectedChipBuilder |
null |
Custom chip renderer for selected items in multi-select |
itemBuilder |
SelectItemBuilder |
null |
Custom renderer for each option in the menu |
emptyBuilder |
EmptyStateBuilder? |
null |
Custom widget when no options match the search |
loadingBuilder |
LoadingBuilder? |
null |
Custom widget shown while onSearch or onLoadMore is pending |
Layout Modes
Single Select
The default mode for selecting a single item. It displays the label of the selected option in the trigger.
WSelect(
value: 'apple',
options: [
SelectOption(value: 'apple', label: 'Apple'),
SelectOption(value: 'banana', label: 'Banana'),
],
onChange: (v) => print(v),
)
Multi Select
Enable isMulti: true to allow multiple selections. By default, it displays selected items as chips.
WSelect(
isMulti: true,
values: ['red', 'blue'],
options: [
SelectOption(value: 'red', label: 'Red'),
SelectOption(value: 'blue', label: 'Blue'),
SelectOption(value: 'green', label: 'Green'),
],
onMultiChange: (list) => print(list),
)
Event Handling
WSelect provides callbacks for selection changes and remote interactions.
WSelect(
onChange: (value) {
// Handle single selection
},
onSearch: (query) async {
// Return list of options based on search query
return fetchRemoteOptions(query);
},
onLoadMore: () async {
// Load next page of options
return fetchNextPage();
},
onOpen: resetCursorToPageOne,
)
Pagination and reopening
Opening the menu resets this widget's own view of the list: the search is
cleared and the visible options go back to options. Rows that arrived through
onLoadMore were appended to the visible list only, so they are gone.
A caller holding a page cursor keeps it across that reset unless onOpen tells
it, and the next scroll to the bottom then asks for the page AFTER the one the
reader can see. Measured on a searchable select over a paged endpoint: open,
scroll once to pull page two, close, reopen, and page two's rows were
unreachable without searching for them.
The reset advice assumes options goes back to page one along with the visible
list, which is the ordinary shape: return rows from onLoadMore and leave
options alone. A caller that instead MIRRORS fetched pages into options
already has all of them on reopen, so resetting its cursor there would re-fetch
page two and append duplicates. Reset the cursor only if options resets too.
Accessibility
The closed trigger is one button whose accessible name is its visible text: the selected option's label (labels joined with , in multi-select), and the placeholder only while nothing is selected. A test or dusk script that finds a select with a value by its placeholder text must look for the selected label instead. The default placeholder is English and wind has no localization layer, so pass a translated placeholder in a localized app.
The closed trigger publishes exactly one semantics node: it carries the button role, the enabled state, the name, the tap action that opens or closes the menu (absent when disabled) and the keyboard focus state. The trigger's own content, including multi-select chips and the output of triggerBuilder / multiTriggerBuilder, is excluded from semantics so it is not announced a second time. The search field of an open menu keeps its own semantics.
State Variants
WSelect automatically manages several states that you can style using prefixes in className:
hover:- When the mouse is over the trigger.focus:- When the dropdown is open, or when the trigger has keyboard focus (Tab). Never whiledisabled; a disabled select is skipped by Tab.disabled:- When the widget is disabled.selected:- When a value is selected.
WSelect(
className: 'border-gray-300 hover:border-blue-500 focus:ring-2 focus:ring-blue-200',
)
Styling Examples
Searchable Select
Combine searchable: true with custom menu styling for a robust selection experience.
WSelect(
searchable: true,
searchPlaceholder: 'Search countries...',
menuClassName: 'bg-white shadow-xl rounded-xl border border-gray-100',
options: countries,
onChange: (val) => _country = val,
)
Tagging (Create Option)
Allow users to create new options if a search yields no results.
WSelect(
searchable: true,
onCreateOption: (query) async {
final newOpt = SelectOption(value: query.toLowerCase(), label: query);
// Add to your local state/database
return newOpt;
},
options: existingTags,
)
All Supported Classes
| Category | Classes |
|---|---|
| Layout | flex, hidden, w-{size}, h-{size} |
| Spacing | p-{n}, m-{n}, gap-{n} |
| Visuals | bg-{color}, border, rounded, shadow |
| States | hover:, focus:, disabled:, dark: |
| Custom | open:, selected: |
Customizing Theme
The default styling for WSelect and its chips can be influenced by the WindThemeData.
WindThemeData(
colors: {
'primary': Colors.indigo,
},
)
Related Documentation
- WFormSelect - Form-integrated version with validation
- WPopover - The underlying overlay engine
- WInput - Used for the internal search field