Skip to content
HyperUI for Flutter requires the Style package — this site itself ships no pub package.

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,
),
),
),
),
),
],
),
),
],
),
);
}
}

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,
),
),
),
),
),
],
),
),
],
),
);
}
}

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,
),
),
),
),
),
],
),
),
);
}
}