Filters
Ported from HyperUI’s Filters — see Attribution.
Dropdown
Section titled “Dropdown”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, ), ), ), ], ), ), ], ); }, ), ], ), ], ), ], ), ); }}<div class="flex gap-4 sm:gap-6"> <details class="group relative"> <summary class="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 [&::-webkit-details-marker]:hidden" > <span class="text-sm font-medium"> Availability </span>
<span class="transition-transform group-open:-rotate-180"> <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.25l-7.5 7.5-7.5-7.5" /> </svg> </span> </summary>
<div class="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" > <div class="flex items-center justify-between px-3 py-2"> <span class="text-sm text-gray-700"> 0 Selected </span>
<button type="button" class="text-sm text-gray-700 underline transition-colors hover:text-gray-900" > Reset </button> </div>
<fieldset class="p-3"> <legend class="sr-only">Checkboxes</legend>
<div class="flex flex-col items-start gap-3"> <label for="Option1" class="inline-flex items-center gap-3"> <input type="checkbox" class="size-5 rounded border-gray-300 shadow-sm" id="Option1" />
<span class="text-sm font-medium text-gray-700"> Option 1 </span> </label>
<label for="Option2" class="inline-flex items-center gap-3"> <input type="checkbox" class="size-5 rounded border-gray-300 shadow-sm" id="Option2" />
<span class="text-sm font-medium text-gray-700"> Option 2 </span> </label>
<label for="Option3" class="inline-flex items-center gap-3"> <input type="checkbox" class="size-5 rounded border-gray-300 shadow-sm" id="Option3" />
<span class="text-sm font-medium text-gray-700"> Option 3 </span> </label> </div> </fieldset> </div> </details>
<details class="group relative"> <summary class="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 [&::-webkit-details-marker]:hidden" > <span class="text-sm font-medium"> Price </span>
<span class="transition-transform group-open:-rotate-180"> <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.25l-7.5 7.5-7.5-7.5" /> </svg> </span> </summary>
<div class="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" > <div class="flex items-center justify-between px-3 py-2"> <span class="text-sm text-gray-700"> Max price is $600 </span>
<button type="button" class="text-sm text-gray-700 underline transition-colors hover:text-gray-900" > Reset </button> </div>
<div class="flex items-center gap-3 p-3"> <label for="MinPrice"> <span class="text-sm text-gray-700"> Min </span>
<input type="number" id="MinPrice" value="0" class="mt-0.5 w-full rounded border-gray-300 shadow-sm sm:text-sm" /> </label>
<label for="MaxPrice"> <span class="text-sm text-gray-700"> Max </span>
<input type="number" id="MaxPrice" value="600" class="mt-0.5 w-full rounded border-gray-300 shadow-sm sm:text-sm" /> </label> </div> </div> </details></div>Accordion
Section titled “Accordion”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, ), ), ), ], ), ), ], ); }, ), ], ), ], ), ], ), ); }}<div class="space-y-4"> <details class="group relative overflow-hidden rounded border border-gray-300 shadow-sm"> <summary class="flex items-center justify-between gap-2 p-3 text-gray-700 transition-colors hover:text-gray-900 [&::-webkit-details-marker]:hidden" > <span class="text-sm font-medium"> Availability </span>
<span class="transition-transform group-open:-rotate-180"> <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.25l-7.5 7.5-7.5-7.5" /> </svg> </span> </summary>
<div class="divide-y divide-gray-300 border-t border-gray-300 bg-white"> <div class="flex items-center justify-between px-3 py-2"> <span class="text-sm text-gray-700"> 0 Selected </span>
<button type="button" class="text-sm text-gray-700 underline transition-colors hover:text-gray-900" > Reset </button> </div>
<fieldset class="p-3"> <legend class="sr-only">Checkboxes</legend>
<div class="flex flex-col items-start gap-3"> <label for="Option1" class="inline-flex items-center gap-3"> <input type="checkbox" class="size-5 rounded border-gray-300 shadow-sm" id="Option1" />
<span class="text-sm font-medium text-gray-700"> Option 1 </span> </label>
<label for="Option2" class="inline-flex items-center gap-3"> <input type="checkbox" class="size-5 rounded border-gray-300 shadow-sm" id="Option2" />
<span class="text-sm font-medium text-gray-700"> Option 2 </span> </label>
<label for="Option3" class="inline-flex items-center gap-3"> <input type="checkbox" class="size-5 rounded border-gray-300 shadow-sm" id="Option3" />
<span class="text-sm font-medium text-gray-700"> Option 3 </span> </label> </div> </fieldset> </div> </details>
<details class="group relative overflow-hidden rounded border border-gray-300 shadow-sm"> <summary class="flex items-center justify-between gap-2 p-3 text-gray-700 transition-colors hover:text-gray-900 [&::-webkit-details-marker]:hidden" > <span class="text-sm font-medium"> Price </span>
<span class="transition-transform group-open:-rotate-180"> <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.25l-7.5 7.5-7.5-7.5" /> </svg> </span> </summary>
<div class="divide-y divide-gray-300 border-t border-gray-300 bg-white"> <div class="flex items-center justify-between px-3 py-2"> <span class="text-sm text-gray-700"> Max price is $600 </span>
<button type="button" class="text-sm text-gray-700 underline transition-colors hover:text-gray-900" > Reset </button> </div>
<div class="flex items-center gap-3 p-3"> <label for="MinPrice"> <span class="text-sm text-gray-700"> Min </span>
<input type="number" id="MinPrice" value="0" class="mt-0.5 w-full rounded border-gray-300 shadow-sm sm:text-sm" /> </label>
<label for="MaxPrice"> <span class="text-sm text-gray-700"> Max </span>
<input type="number" id="MaxPrice" value="600" class="mt-0.5 w-full rounded border-gray-300 shadow-sm sm:text-sm" /> </label> </div> </div> </details></div>