Selects
Ported from HyperUI’s Selects — see Attribution.
Includes a dark: variant — toggle the site theme (top right) to preview it.
class NeobrutalismSelects1Example extends StatelessWidget { const NeobrutalismSelects1Example({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) { 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 textColor = isDark ? Colors.white : Colors.black; final chevronSize = fontSize * 1.5;
return Style( 'mx-auto max-w-xs p-6', child: Style( 'text-black dark:text-white', display: DisplayType.block, children: [ const Style( 'text-sm font-semibold', display: DisplayType.inline, child: Text('Headliner'), ), Style( 'mt-0.5 w-full border-2 border-black bg-white placeholder-black shadow-[4px_4px_0_0] shadow-black focus:ring-2 focus:ring-yellow-300 sm:text-sm dark:border-white dark:bg-gray-900 dark:placeholder-white dark:shadow-white dark:focus:ring-yellow-600', child: 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: false, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, contentPadding: EdgeInsets.fromLTRB( 12, isSmUp ? 6 : 8, 40, isSmUp ? 6 : 8, ), ), 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" class="text-black"> <span class="text-sm font-semibold"> Headliner </span>
<select name="Headline" id="Headline" class="mt-0.5 w-full border-2 border-black bg-white placeholder-black shadow-[4px_4px_0_0] shadow-black focus:ring-2 focus:ring-yellow-300 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”Includes a dark: variant — toggle the site theme (top right) to preview it.
class NeobrutalismSelects2Example extends StatelessWidget { const NeobrutalismSelects2Example({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) { 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 textColor = isDark ? Colors.white : Colors.black; final chevronSize = fontSize * 1.5;
return Style( 'mx-auto max-w-xs p-6', child: Style( 'text-black dark:text-white', display: DisplayType.block, children: [ const Style( 'text-sm font-semibold', display: DisplayType.inline, child: Text('Headliner'), ), Style( 'mt-0.5 w-full border-2 border-black bg-white placeholder-black shadow-[4px_4px_0_0] shadow-black focus:ring-2 focus:ring-yellow-300 sm:text-sm dark:border-white dark:bg-gray-900 dark:placeholder-white dark:shadow-white dark:focus:ring-yellow-600', child: 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: false, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, contentPadding: EdgeInsets.fromLTRB( 12, isSmUp ? 6 : 8, 40, isSmUp ? 6 : 8, ), ), 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" class="text-black"> <span class="text-sm font-semibold"> Headliner </span>
<select name="Headline" id="Headline" class="mt-0.5 w-full border-2 border-black bg-white placeholder-black shadow-[4px_4px_0_0] shadow-black focus:ring-2 focus:ring-yellow-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 aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"/></svg>''';
class NeobrutalismSelects3Example extends StatefulWidget { const NeobrutalismSelects3Example({super.key});
@override State<NeobrutalismSelects3Example> createState() => _NeobrutalismSelects3ExampleState();}
class _NeobrutalismSelects3ExampleState extends State<NeobrutalismSelects3Example> { 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', child: Style( 'text-black dark:text-white', display: DisplayType.block, children: [ const Style( 'text-sm font-semibold', display: DisplayType.inline, child: Text('Headliner'), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _focusNode.requestFocus(), child: Style( 'relative', children: [ Style( 'mt-0.5 w-full border-2 border-black bg-white placeholder-black shadow-[4px_4px_0_0] shadow-black focus:ring-2 focus:ring-yellow-300 sm:text-sm dark:border-white dark:bg-gray-900 dark:placeholder-white dark:shadow-white dark:focus:ring-yellow-600 [&::-webkit-calendar-picker-indicator]:opacity-0', child: TextFormField( controller: _controller, focusNode: _focusNode, decoration: InputDecoration( hintText: 'Please select', hintStyle: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: isDark ? Colors.white : Colors.black, ), ), style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: isDark ? Colors.white : Colors.black, ), ), ), const Style( 'absolute inset-y-0 right-0 grid w-8 place-content-center', child: Style( 'size-4', child: CurrentColorIcon(_chevronSvg), ), ), ], ), ), if (_isOpen && filtered.isNotEmpty) TextFieldTapRegion( child: _NeobrutalismComboboxOptionsPanel( options: filtered, isDark: isDark, fontSize: fontSize, lineHeight: lineHeight, onSelect: _selectOption, ), ), ], ), ), ); }}
class _NeobrutalismComboboxOptionsPanel extends StatelessWidget { const _NeobrutalismComboboxOptionsPanel({ 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 ? Colors.white : Colors.black; 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, width: 2), boxShadow: [ BoxShadow(color: borderColor, offset: const Offset(4, 4)), ], ), 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" class="text-black"> <span class="text-sm font-semibold"> Headliner </span>
<div class="relative"> <input type="text" id="Headline" list="HeadlineList" placeholder="Please select" class="mt-0.5 w-full border-2 border-black bg-white placeholder-black shadow-[4px_4px_0_0] shadow-black focus:ring-2 focus:ring-yellow-300 sm:text-sm [&::-webkit-calendar-picker-indicator]:opacity-0" />
<span class="absolute inset-y-0 right-0 grid w-8 place-content-center"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4" > <path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /> </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>