Selects
Ported from HyperUI’s Selects — see Attribution.
Includes a dark: variant — toggle the site theme (top right) to preview it.
class Selects1Example extends StatelessWidget { const Selects1Example({super.key});
static const _options = <String, String>{ '': 'Please select', 'JM': 'John Mayer', 'SRV': 'Stevie Ray Vaughn', 'JH': 'Jimi Hendrix', 'BBK': 'B.B King', 'AK': 'Albert King', 'BG': 'Buddy Guy', 'EC': 'Eric Clapton', };
static const _chevronSvg = '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="#6b7280" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 8l4 4 4-4"/></svg>''';
@override Widget build(BuildContext context) { return Style( 'mx-auto max-w-xs p-6', children: [ const Style( 'text-sm font-medium text-gray-700 dark:text-gray-200', display: DisplayType.inline, child: Text('Headliner'), ), Style( 'mt-0.5 w-full rounded border-gray-300 shadow-sm sm:text-sm dark:border-gray-600 dark:bg-gray-900 dark:text-white', child: Builder( builder: (context) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSmUp = MediaQuery.sizeOf(context).width >= 640; final borderColor = isDark ? tailwindColors['gray']![600]! : tailwindColors['gray']![300]!; final border = OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: BorderSide(color: borderColor), ); final fontSize = isSmUp ? 14.0 : 16.0; final lineHeight = isSmUp ? 20.0 : 24.0; final baseStyle = Theme.of(context).textTheme.bodyLarge ?? const TextStyle(); final textColor = isDark ? Colors.white : tailwindColors['gray']![900]!; final chevronSize = fontSize * 1.5; return Stack( children: [ DropdownButtonFormField<String>( initialValue: '', isExpanded: true, itemHeight: null, icon: const SizedBox.shrink(), style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: textColor, ), decoration: InputDecoration( isDense: true, filled: true, fillColor: isDark ? tailwindColors['gray']![900] : Colors.white, contentPadding: EdgeInsets.fromLTRB( 12, isSmUp ? 7 : 9, 40, isSmUp ? 7 : 9, ), border: border, enabledBorder: border, focusedBorder: border, ), items: [ for (final entry in _options.entries) DropdownMenuItem(value: entry.key, child: Text(entry.value)), ], onChanged: (_) {}, ), Positioned.fill( child: IgnorePointer( child: Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.only(right: 8), child: SvgPicture.string( _chevronSvg, width: chevronSize, height: chevronSize, ), ), ), ), ), ], ); }, ), ), ], ); }}<label for="Headline"> <span class="text-sm font-medium text-gray-700"> Headliner </span>
<select name="Headline" id="Headline" class="mt-0.5 w-full rounded border-gray-300 shadow-sm sm:text-sm" > <option value="">Please select</option> <option value="JM">John Mayer</option> <option value="SRV">Stevie Ray Vaughn</option> <option value="JH">Jimi Hendrix</option> <option value="BBK">B.B King</option> <option value="AK">Albert King</option> <option value="BG">Buddy Guy</option> <option value="EC">Eric Clapton</option> </select></label>Option groups
Section titled “Option groups”class Selects2Example extends StatelessWidget { const Selects2Example({super.key});
static const _groups = <(String, List<(String, String)>)>[ ('A', [('AK', 'Albert King')]), ('B', [('BBK', 'B.B King'), ('BG', 'Buddy Guy')]), ('E', [('EC', 'Eric Clapton')]), ('J', [('JM', 'John Mayer'), ('JH', 'Jimi Hendrix')]), ('S', [('SRV', 'Stevie Ray Vaughn')]), ];
static const _chevronSvg = '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="#6b7280" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 8l4 4 4-4"/></svg>''';
@override Widget build(BuildContext context) { return Style( 'mx-auto max-w-xs p-6', children: [ const Style( 'text-sm font-medium text-gray-700', display: DisplayType.inline, child: Text('Headliner'), ), Style( 'mt-0.5 w-full rounded-lg border-gray-300 sm:text-sm', child: Builder( builder: (context) { final isSmUp = MediaQuery.sizeOf(context).width >= 640; final border = OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: tailwindColors['gray']![300]!), ); final fontSize = isSmUp ? 14.0 : 16.0; final lineHeight = isSmUp ? 20.0 : 24.0; final baseStyle = Theme.of(context).textTheme.bodyLarge ?? const TextStyle(); final chevronSize = fontSize * 1.5; return Stack( children: [ DropdownButtonFormField<String>( initialValue: '', isExpanded: true, itemHeight: null, icon: const SizedBox.shrink(), style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: tailwindColors['gray']![900]!, ), decoration: InputDecoration( isDense: true, filled: true, fillColor: Colors.white, contentPadding: EdgeInsets.fromLTRB( 12, isSmUp ? 7 : 9, 40, isSmUp ? 7 : 9, ), border: border, enabledBorder: border, focusedBorder: border, ), items: [ const DropdownMenuItem(value: '', child: Text('Please select')), for (final group in _groups) ...[ DropdownMenuItem( enabled: false, value: '__group_${group.$1}', child: Text( group.$1, style: const TextStyle(fontWeight: FontWeight.bold), ), ), for (final option in group.$2) DropdownMenuItem(value: option.$1, child: Text(option.$2)), ], ], onChanged: (_) {}, ), Positioned.fill( child: IgnorePointer( child: Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.only(right: 8), child: SvgPicture.string( _chevronSvg, width: chevronSize, height: chevronSize, ), ), ), ), ), ], ); }, ), ), ], ); }}<label for="Headline"> <span class="text-sm font-medium text-gray-700"> Headliner </span>
<select name="Headline" id="Headline" class="mt-0.5 w-full rounded-lg border-gray-300 sm:text-sm" > <option value="">Please select</option>
<optgroup label="A"> <option value="AK">Albert King</option> </optgroup>
<optgroup label="B"> <option value="BBK">B.B King</option> <option value="BG">Buddy Guy</option> </optgroup>
<optgroup label="E"> <option value="EC">Eric Clapton</option> </optgroup>
<optgroup label="J"> <option value="JM">John Mayer</option> <option value="JH">Jimi Hendrix</option> </optgroup>
<optgroup label="S"> <option value="SRV">Stevie Ray Vaughn</option> </optgroup> </select></label>Datalist
Section titled “Datalist”Includes a dark: variant — toggle the site theme (top right) to preview it.
class CurrentColorIcon extends StatelessWidget implements StyleReplacedElementWrapper { const CurrentColorIcon(this.svg, {super.key});
final String svg;
@override Widget build(BuildContext context) { final color = IconTheme.of(context).color ?? const Color(0xFF000000); return SvgPicture.string(svg, theme: SvgTheme(currentColor: color)); }}
const _chevronSvg = '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg>''';
class Selects3Example extends StatefulWidget { const Selects3Example({super.key});
@override State<Selects3Example> createState() => _Selects3ExampleState();}
class _Selects3ExampleState extends State<Selects3Example> { static const _options = <String>[ 'John Mayer', 'Stevie Ray Vaughn', 'Jimi Hendrix', 'B.B King', 'Albert King', 'Buddy Guy', 'Eric Clapton', ];
final _controller = TextEditingController(); final _focusNode = FocusNode(); var _isOpen = false;
@override void initState() { super.initState(); _focusNode.addListener(_handleFocusChange); _controller.addListener(_handleTextChange); }
@override void dispose() { _focusNode.removeListener(_handleFocusChange); _focusNode.dispose(); _controller.removeListener(_handleTextChange); _controller.dispose(); super.dispose(); }
void _handleFocusChange() { setState(() => _isOpen = _focusNode.hasFocus); }
void _handleTextChange() { setState(() {}); }
List<String> get _filteredOptions { final query = _controller.text.trim().toLowerCase(); if (query.isEmpty) return _options; return [ for (final option in _options) if (option.toLowerCase().contains(query)) option, ]; }
void _selectOption(String option) { _controller.value = TextEditingValue( text: option, selection: TextSelection.collapsed(offset: option.length), ); _focusNode.unfocus(); }
@override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSmUp = MediaQuery.sizeOf(context).width >= 640; final fontSize = isSmUp ? 14.0 : 16.0; final lineHeight = isSmUp ? 20.0 : 24.0; final baseStyle = Theme.of(context).textTheme.bodyLarge ?? const TextStyle(); final filtered = _filteredOptions;
return TapRegion( onTapOutside: (_) => _focusNode.unfocus(), child: Style( 'mx-auto max-w-xs p-6', children: [ const Style( 'text-sm font-medium text-gray-700 dark:text-gray-200', display: DisplayType.inline, child: Text('Headliner'), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _focusNode.requestFocus(), child: Style( 'relative', children: [ Style( 'mt-0.5 w-full rounded border-gray-300 pe-8 shadow-sm sm:text-sm dark:border-gray-600 dark:bg-gray-900 dark:text-white [&::-webkit-calendar-picker-indicator]:opacity-0', child: TextFormField( controller: _controller, focusNode: _focusNode, decoration: const InputDecoration( hintText: 'Please select', ), style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: isDark ? Colors.white : Colors.black, ), ), ), Style( 'absolute inset-y-0 right-0 grid w-8 place-content-center text-gray-700 dark:text-gray-200', child: Style( 'size-4', child: CurrentColorIcon(_chevronSvg), ), ), ], ), ), if (_isOpen && filtered.isNotEmpty) TextFieldTapRegion( child: _ComboboxOptionsPanel( options: filtered, isDark: isDark, fontSize: fontSize, lineHeight: lineHeight, onSelect: _selectOption, ), ), ], ), ); }}
class _ComboboxOptionsPanel extends StatelessWidget { const _ComboboxOptionsPanel({ required this.options, required this.isDark, required this.fontSize, required this.lineHeight, required this.onSelect, });
final List<String> options; final bool isDark; final double fontSize; final double lineHeight; final ValueChanged<String> onSelect;
@override Widget build(BuildContext context) { final borderColor = isDark ? tailwindColors['gray']![600]! : tailwindColors['gray']![300]!; final textColor = isDark ? Colors.white : Colors.black; return Container( margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: isDark ? tailwindColors['gray']![900] : Colors.white, border: Border.all(color: borderColor), borderRadius: BorderRadius.circular(4), boxShadow: const [ BoxShadow( color: Color(0x1A000000), offset: Offset(0, 1), blurRadius: 2, ), ], ), clipBehavior: Clip.antiAlias, child: Material( type: MaterialType.transparency, child: Column( mainAxisSize: MainAxisSize.min, children: [ for (final option in options) InkWell( onTap: () => onSelect(option), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Align( alignment: AlignmentDirectional.centerStart, child: Text( option, style: TextStyle( fontSize: fontSize, height: lineHeight / fontSize, color: textColor, ), ), ), ), ), ], ), ), ); }}<label for="Headline"> <span class="text-sm font-medium text-gray-700"> Headliner </span>
<div class="relative"> <input type="text" id="Headline" list="HeadlineList" placeholder="Please select" class="mt-0.5 w-full rounded border-gray-300 pe-8 shadow-sm sm:text-sm [&::-webkit-calendar-picker-indicator]:opacity-0" />
<span class="absolute inset-y-0 right-0 grid w-8 place-content-center text-gray-700"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4" > <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> </svg> </span> </div>
<datalist name="Headline" id="HeadlineList"> <option value="JM">John Mayer</option> <option value="SRV">Stevie Ray Vaughn</option> <option value="JH">Jimi Hendrix</option> <option value="BBK">B.B King</option> <option value="AK">Albert King</option> <option value="BG">Buddy Guy</option> <option value="EC">Eric Clapton</option> </datalist></label>