Choice Chips
Single-select chip group — lavender selected chip, pill shape, wraps across lines
View component →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.
What's Included
Use Cases
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.
Copy this into your Flutter project. No external packages required.
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) // disabledSingle-select chip group — lavender selected chip, pill shape, wraps across lines
View component →One enum-driven chip — basic, input, choice & status kinds, lavender selected state
View component →Tristate checkbox — unchecked, checked & indeterminate, with label, disabled & error states
View component →