SELECTION CONTROLS

Filter Chips

A single, reusable Flutter filter-chip group driven by a generic list of options and a Set of selected values. Any number of chips can be on at once — each selected chip grows a lavender leading check, a lavender tint, and a lavender border, while the rest stay muted-grey. The chips are pill-shaped, touch-sized, and wrap across lines. No external packages, fully DartPad-ready.

chipsfiltermulti-select
Preview5 variants

One on

OpenIn progressDoneArchived

Several on

OpenIn progressDoneArchived

None on

OpenIn progressDoneArchived

What's Included

  • Multi-selection backed by a Set of values
  • Lavender leading check icon on each selected chip
  • Lavender-tinted selected chips with a matching border
  • Muted-grey unselected chips with a hairline border
  • Wrap layout that reflows chips across lines
  • One AppFilterChips widget driven by a Set + onToggle

Use Cases

  • Faceted filters — status, tags, or categories on a list
  • Multi-answer survey or interest pickers
  • Active-filter bars where several can be on
  • Tag selection when composing or editing a record

PRO TIP

Filter chips toggle independently, so back them with a Set rather than a single value, and add or remove on each tap. Show the leading check only on selected chips so the on/off state reads instantly without color alone, and keep an easy way to clear all filters — a multi-select with no reset traps users in a narrow view.

Complete Dart Code

Copy this into your Flutter project. No external packages required.

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final Set<String> _filters = {'Open'};

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: const Color(0xFFFAFAFB),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: AppFilterChips<String>(
              selected: _filters,
              onToggle: (v) => setState(() {
                if (_filters.contains(v)) {
                  _filters.remove(v);
                } else {
                  _filters.add(v);
                }
              }),
              options: const [
                ChipOption(value: 'Open', label: 'Open'),
                ChipOption(value: 'In progress', label: 'In progress'),
                ChipOption(value: 'Done', label: 'Done'),
                ChipOption(value: 'Archived', label: 'Archived'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

/// FlutterKit Filter Chips — house design system (Linear lavender, light).
/// Multi-select chip group backed by a Set; selected chips show a check.
/// rohansurve.in/flutterkit/filter-chips

class ChipOption<T> {
  const ChipOption({required this.value, required this.label});

  /// The value this chip toggles.
  final T value;

  /// Chip label.
  final String label;
}

class AppFilterChips<T> extends StatelessWidget {
  const AppFilterChips({
    super.key,
    required this.options,
    required this.selected,
    this.onToggle,
    this.enabled = true,
  });

  /// The selectable options.
  final List<ChipOption<T>> options;

  /// The set of currently selected values.
  final Set<T> selected;

  /// Called with the toggled value on tap.
  final ValueChanged<T>? onToggle;

  /// Disabled look — taps ignored.
  final bool enabled;

  // ── House tokens — docs/flutterkit-design.md §1 ──
  static const Color _accent = Color(0xFF5E6AD2);
  static const Color _accentTint = Color(0xFFEEF0FB);
  static const Color _accentBorder = Color(0xFFD5D9F4);
  static const Color _surfaceMuted = Color(0xFFF4F5F7);
  static const Color _hairline = Color(0xFFE6E8EB);
  static const Color _ink = Color(0xFF1C1E26);

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8,
      runSpacing: 8,
      children: options.map((o) {
        final isOn = selected.contains(o.value);
        final fg = isOn ? _accent : _ink;
        final fill = isOn ? _accentTint : _surfaceMuted;
        final border = isOn ? _accentBorder : _hairline;
        return Opacity(
          opacity: enabled ? 1 : 0.5,
          child: Material(
            color: Colors.transparent,
            child: InkWell(
              borderRadius: BorderRadius.circular(999),
              onTap: enabled && onToggle != null
                  ? () => onToggle!(o.value)
                  : null,
              child: AnimatedContainer(
                duration: const Duration(milliseconds: 140),
                padding: EdgeInsets.only(
                  left: isOn ? 8 : 14,
                  right: 14,
                  top: 8,
                  bottom: 8,
                ),
                decoration: BoxDecoration(
                  color: fill,
                  borderRadius: BorderRadius.circular(999),
                  border: Border.all(color: border),
                ),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    if (isOn) ...[
                      const Icon(Icons.check, size: 16, color: _accent),
                      const SizedBox(width: 4),
                    ],
                    Text(
                      o.label,
                      style: TextStyle(
                        color: fg,
                        fontSize: 13,
                        fontWeight: FontWeight.w500,
                        height: 1.2,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      }).toList(),
    );
  }
}

// ── Usage ──
// AppFilterChips<String>(selected: set, options: const [...], onToggle: (v) {})
// ChipOption(value: 'Open', label: 'Open')                         // one option
// AppFilterChips(..., enabled: false)                              // disabled

How to use this widget

  1. 1Copy the complete Dart code above
  2. 2Create a new file — app_filter_chips.dart in your Flutter project
  3. 3Use AppFilterChips<String>(selected: set, options: const [...], onToggle: (v) {}) — switch the look with the options and enabled parameters

Related components

Selection Controls

Choice Chips

Single-select chip group — lavender selected chip, pill shape, wraps across lines

View component →
Avatars & Chips

Chip

One enum-driven chip — basic, input, choice & status kinds, lavender selected state

View component →
Selection Controls

Checkbox

Tristate checkbox — unchecked, checked & indeterminate, with label, disabled & error states

View component →