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

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