search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

WSelect

A highly customizable, utility-first select component that supports single selection, multi-selection, searching, and remote data fetching.

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>> Function(String)? null Async remote search handler; returns filtered options for the query
onCreateOption Future> Function(String)? 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>> Function()? 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 while disabled; 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,
  },
)
  • WFormSelect - Form-integrated version with validation
  • WPopover - The underlying overlay engine
  • WInput - Used for the internal search field