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

Filters

Ported from HyperUI’s Filters — see Attribution.

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 _chevronDownSvg =
'''<svg aria-hidden="true" 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.25l-7.5 7.5-7.5-7.5"/></svg>''';
class Filters1Example extends StatefulWidget {
const Filters1Example({super.key});
@override
State<Filters1Example> createState() => _Filters1ExampleState();
}
class _Filters1ExampleState extends State<Filters1Example> {
var _availabilityOpen = false;
var _priceOpen = false;
var _checked0 = false;
var _checked1 = false;
var _checked2 = false;
@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 : const Color(0xFF000000);
return Style(
'mx-auto max-w-md p-6',
child: Style(
'flex gap-4 sm:gap-6',
children: [
Style(
'group relative',
open: _availabilityOpen,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _availabilityOpen = !_availabilityOpen),
child: Style(
'flex items-center gap-2 border-b border-gray-300 pb-1 text-gray-700 transition-colors hover:border-gray-400 hover:text-gray-900 dark:border-gray-600 dark:text-gray-200 dark:hover:border-gray-700 dark:hover:text-white [&::-webkit-details-marker]:hidden',
cursor: SystemMouseCursors.click,
children: [
const Style(
'text-sm font-medium',
display: DisplayType.inline,
child: Text('Availability'),
),
Style(
'transition-transform group-open:-rotate-180',
child: const Style(
'size-4',
child: CurrentColorIcon(_chevronDownSvg),
),
),
],
),
),
if (_availabilityOpen)
Style(
'z-auto w-64 divide-y divide-gray-300 rounded border border-gray-300 bg-white shadow-sm group-open:absolute group-open:start-0 group-open:top-8 dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-900',
children: [
Style(
'flex items-center justify-between px-3 py-2',
children: const [
Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('0 Selected'),
),
Style(
'text-sm text-gray-700 underline transition-colors hover:text-gray-900 dark:text-gray-200 dark:hover:text-white',
child: Text('Reset'),
),
],
),
Style(
'p-3',
children: [
const Style('sr-only', child: Text('Checkboxes')),
Style(
'flex flex-col items-start gap-3',
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _checked0 = !_checked0),
child: Style(
'inline-flex items-center gap-3',
cursor: SystemMouseCursors.click,
children: [
Style(
'size-5 rounded border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-900 dark:ring-offset-gray-900 dark:checked:bg-blue-600',
formControl: true,
checked: _checked0,
),
const Style(
'text-sm font-medium text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Option 1'),
),
],
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _checked1 = !_checked1),
child: Style(
'inline-flex items-center gap-3',
cursor: SystemMouseCursors.click,
children: [
Style(
'size-5 rounded border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-900 dark:ring-offset-gray-900 dark:checked:bg-blue-600',
formControl: true,
checked: _checked1,
),
const Style(
'text-sm font-medium text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Option 2'),
),
],
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _checked2 = !_checked2),
child: Style(
'inline-flex items-center gap-3',
cursor: SystemMouseCursors.click,
children: [
Style(
'size-5 rounded border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-900 dark:ring-offset-gray-900 dark:checked:bg-blue-600',
formControl: true,
checked: _checked2,
),
const Style(
'text-sm font-medium text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Option 3'),
),
],
),
),
],
),
],
),
],
),
],
),
Style(
'group relative',
open: _priceOpen,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _priceOpen = !_priceOpen),
child: Style(
'flex items-center gap-2 border-b border-gray-300 pb-1 text-gray-700 transition-colors hover:border-gray-400 hover:text-gray-900 dark:border-gray-600 dark:text-gray-200 dark:hover:border-gray-700 dark:hover:text-white [&::-webkit-details-marker]:hidden',
cursor: SystemMouseCursors.click,
children: [
const Style(
'text-sm font-medium',
display: DisplayType.inline,
child: Text('Price'),
),
Style(
'transition-transform group-open:-rotate-180',
child: const Style(
'size-4',
child: CurrentColorIcon(_chevronDownSvg),
),
),
],
),
),
if (_priceOpen)
Style(
'z-auto w-64 divide-y divide-gray-300 rounded border border-gray-300 bg-white shadow-sm group-open:absolute group-open:start-0 group-open:top-8 dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-900',
children: [
Style(
'flex items-center justify-between px-3 py-2',
children: const [
Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Max price is \$600'),
),
Style(
'text-sm text-gray-700 underline transition-colors hover:text-gray-900 dark:text-gray-200 dark:hover:text-white',
child: Text('Reset'),
),
],
),
LayoutBuilder(
builder: (context, constraints) {
final fieldWidth =
((constraints.maxWidth - 24.0 - 12.0) / 2).clamp(
0.0,
double.infinity,
);
return Style(
'flex items-center gap-3 p-3',
children: [
SizedBox(
width: fieldWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Min'),
),
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: TextFormField(
initialValue: '0',
keyboardType: TextInputType.number,
style: baseStyle.copyWith(
fontSize: fontSize,
height: lineHeight / fontSize,
color: textColor,
),
),
),
],
),
),
SizedBox(
width: fieldWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Max'),
),
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: TextFormField(
initialValue: '600',
keyboardType: TextInputType.number,
style: baseStyle.copyWith(
fontSize: fontSize,
height: lineHeight / fontSize,
color: textColor,
),
),
),
],
),
),
],
);
},
),
],
),
],
),
],
),
);
}
}

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 _chevronDownSvg =
'''<svg aria-hidden="true" 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.25l-7.5 7.5-7.5-7.5"/></svg>''';
class Filters2Example extends StatefulWidget {
const Filters2Example({super.key});
@override
State<Filters2Example> createState() => _Filters2ExampleState();
}
class _Filters2ExampleState extends State<Filters2Example> {
var _availabilityOpen = false;
var _priceOpen = false;
var _checked0 = false;
var _checked1 = false;
var _checked2 = false;
@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 : const Color(0xFF000000);
return Style(
'mx-auto max-w-md p-6',
child: Style(
'space-y-4',
children: [
Style(
'group relative overflow-hidden rounded border border-gray-300 shadow-sm dark:border-gray-600',
open: _availabilityOpen,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _availabilityOpen = !_availabilityOpen),
child: Style(
'flex items-center justify-between gap-2 p-3 text-gray-700 transition-colors hover:text-gray-900 dark:text-gray-200 dark:hover:text-white [&::-webkit-details-marker]:hidden',
cursor: SystemMouseCursors.click,
children: [
const Style(
'text-sm font-medium',
display: DisplayType.inline,
child: Text('Availability'),
),
Style(
'transition-transform group-open:-rotate-180',
child: const Style(
'size-4',
child: CurrentColorIcon(_chevronDownSvg),
),
),
],
),
),
if (_availabilityOpen)
Style(
'divide-y divide-gray-300 border-t border-gray-300 bg-white dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-900',
children: [
const Style(
'flex items-center justify-between px-3 py-2',
children: [
Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('0 Selected'),
),
Style(
'text-sm text-gray-700 underline transition-colors hover:text-gray-900 dark:text-gray-200 dark:hover:text-white',
child: Text('Reset'),
),
],
),
Style(
'p-3',
children: [
const Style('sr-only', child: Text('Checkboxes')),
Style(
'flex flex-col items-start gap-3',
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _checked0 = !_checked0),
child: Style(
'inline-flex items-center gap-3',
cursor: SystemMouseCursors.click,
children: [
Style(
'size-5 rounded border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-900 dark:ring-offset-gray-900 dark:checked:bg-blue-600',
formControl: true,
checked: _checked0,
),
const Style(
'text-sm font-medium text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Option 1'),
),
],
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _checked1 = !_checked1),
child: Style(
'inline-flex items-center gap-3',
cursor: SystemMouseCursors.click,
children: [
Style(
'size-5 rounded border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-900 dark:ring-offset-gray-900 dark:checked:bg-blue-600',
formControl: true,
checked: _checked1,
),
const Style(
'text-sm font-medium text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Option 2'),
),
],
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
setState(() => _checked2 = !_checked2),
child: Style(
'inline-flex items-center gap-3',
cursor: SystemMouseCursors.click,
children: [
Style(
'size-5 rounded border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-900 dark:ring-offset-gray-900 dark:checked:bg-blue-600',
formControl: true,
checked: _checked2,
),
const Style(
'text-sm font-medium text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Option 3'),
),
],
),
),
],
),
],
),
],
),
],
),
Style(
'group relative overflow-hidden rounded border border-gray-300 shadow-sm dark:border-gray-600',
open: _priceOpen,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _priceOpen = !_priceOpen),
child: Style(
'flex items-center justify-between gap-2 p-3 text-gray-700 transition-colors hover:text-gray-900 dark:text-gray-200 dark:hover:text-white [&::-webkit-details-marker]:hidden',
cursor: SystemMouseCursors.click,
children: [
const Style(
'text-sm font-medium',
display: DisplayType.inline,
child: Text('Price'),
),
Style(
'transition-transform group-open:-rotate-180',
child: const Style(
'size-4',
child: CurrentColorIcon(_chevronDownSvg),
),
),
],
),
),
if (_priceOpen)
Style(
'divide-y divide-gray-300 border-t border-gray-300 bg-white dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-900',
children: [
const Style(
'flex items-center justify-between px-3 py-2',
children: [
Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Max price is \$600'),
),
Style(
'text-sm text-gray-700 underline transition-colors hover:text-gray-900 dark:text-gray-200 dark:hover:text-white',
child: Text('Reset'),
),
],
),
LayoutBuilder(
builder: (context, constraints) {
final fieldWidth =
((constraints.maxWidth - 24.0 - 12.0) / 2).clamp(
0.0,
double.infinity,
);
return Style(
'flex items-center gap-3 p-3',
children: [
SizedBox(
width: fieldWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Min'),
),
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: TextFormField(
initialValue: '0',
keyboardType: TextInputType.number,
style: baseStyle.copyWith(
fontSize: fontSize,
height: lineHeight / fontSize,
color: textColor,
),
),
),
],
),
),
SizedBox(
width: fieldWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Style(
'text-sm text-gray-700 dark:text-gray-200',
display: DisplayType.inline,
child: Text('Max'),
),
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: TextFormField(
initialValue: '600',
keyboardType: TextInputType.number,
style: baseStyle.copyWith(
fontSize: fontSize,
height: lineHeight / fontSize,
color: textColor,
),
),
),
],
),
),
],
);
},
),
],
),
],
),
],
),
);
}
}