InputChip class
A Material Design input chip.
Input chips represent a complex piece of information, such as an entity (person, place, or thing) or conversational text, in a compact form.
Input chips can be made selectable by setting onSelected, deletable by setting onDeleted, and pressable like a button with onPressed. They have a label, and they can have a leading icon (see avatar) and a trailing icon (deleteIcon). Colors and padding can be customized.
Requires one of its ancestors to be a Material widget.
Input chips work together with other UI elements. They can appear:
- In a Wrap widget.
- In a horizontally scrollable list, for example configured such as a ListView with ListView.scrollDirection set to Axis.horizontal.
This example shows how to create InputChips with onSelected and onDeleted callbacks. When the user taps the chip, the chip will be selected. When the user taps the delete icon, the chip will be deleted.
To see it in action, copy and run this code snippet on DartPad.
// Flutter code sample InputChip.
import 'package:material_ui/material_ui.dart';
void main() => runApp(const ChipApp());
class ChipApp extends StatelessWidget {
const ChipApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4)),
home: const InputChipExample(),
);
}
}
class InputChipExample extends StatefulWidget {
const InputChipExample({super.key});
@override
State<InputChipExample> createState() => _InputChipExampleState();
}
class _InputChipExampleState extends State<InputChipExample> {
int inputs = 3;
int? selectedIndex;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('InputChip Sample')),
body: Center(
child: Column(
mainAxisSize: .min,
mainAxisAlignment: .center,
children: <Widget>[
Wrap(
alignment: .center,
spacing: 5.0,
children: List<Widget>.generate(inputs, (int index) {
return InputChip(
label: Text('Person ${index + 1}'),
selected: selectedIndex == index,
onSelected: (bool selected) {
setState(() {
if (selectedIndex == index) {
selectedIndex = null;
} else {
selectedIndex = index;
}
});
},
onDeleted: () {
setState(() {
inputs = inputs - 1;
});
},
);
}).toList(),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
setState(() {
inputs = 3;
});
},
child: const Text('Reset'),
),
],
),
),
);
}
}
The following example shows how to generate InputChips from user text input. When the user enters a pizza topping in the text field, the user is presented with a list of suggestions. When selecting one of the suggestions, an InputChip is generated in the text field.
To see it in action, copy and run this code snippet on DartPad.
import 'dart:async';
import 'package:material_ui/material_ui.dart';
const List<String> _pizzaToppings = <String>[
'Olives',
'Tomato',
'Cheese',
'Pepperoni',
'Bacon',
'Onion',
'Jalapeno',
'Mushrooms',
'Pineapple',
];
void main() => runApp(const EditableChipFieldApp());
class EditableChipFieldApp extends StatelessWidget {
const EditableChipFieldApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: EditableChipFieldExample());
}
}
class EditableChipFieldExample extends StatefulWidget {
const EditableChipFieldExample({super.key});
@override
EditableChipFieldExampleState createState() {
return EditableChipFieldExampleState();
}
}
class EditableChipFieldExampleState extends State<EditableChipFieldExample> {
final FocusNode _chipFocusNode = FocusNode();
List<String> _toppings = <String>[_pizzaToppings.first];
List<String> _suggestions = <String>[];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Editable Chip Field Sample')),
body: Column(
children: <Widget>[
Padding(
padding: const .symmetric(horizontal: 16),
child: ChipsInput<String>(
values: _toppings,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.local_pizza_rounded),
hintText: 'Search for toppings',
),
strutStyle: const StrutStyle(fontSize: 15),
onChanged: _onChanged,
onSubmitted: _onSubmitted,
chipBuilder: _chipBuilder,
onTextChanged: _onSearchChanged,
),
),
if (_suggestions.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: _suggestions.length,
itemBuilder: (BuildContext context, int index) {
return ToppingSuggestion(
_suggestions[index],
onTap: _selectSuggestion,
);
},
),
),
],
),
);
}
Future<void> _onSearchChanged(String value) async {
final List<String> results = await _suggestionCallback(value);
setState(() {
_suggestions = results
.where((String topping) => !_toppings.contains(topping))
.toList();
});
}
Widget _chipBuilder(BuildContext context, String topping) {
return ToppingInputChip(
topping: topping,
onDeleted: _onChipDeleted,
onSelected: _onChipTapped,
);
}
void _selectSuggestion(String topping) {
setState(() {
_toppings.add(topping);
_suggestions = <String>[];
});
}
void _onChipTapped(String topping) {}
void _onChipDeleted(String topping) {
setState(() {
_toppings.remove(topping);
_suggestions = <String>[];
});
}
void _onSubmitted(String text) {
if (text.trim().isNotEmpty) {
setState(() {
_toppings = <String>[..._toppings, text.trim()];
});
} else {
_chipFocusNode.unfocus();
setState(() {
_toppings = <String>[];
});
}
}
void _onChanged(List<String> data) {
setState(() {
_toppings = data;
});
}
FutureOr<List<String>> _suggestionCallback(String text) {
if (text.isNotEmpty) {
return _pizzaToppings.where((String topping) {
return topping.toLowerCase().contains(text.toLowerCase());
}).toList();
}
return const <String>[];
}
}
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
super.key,
required this.values,
this.decoration = const InputDecoration(),
this.style,
this.strutStyle,
required this.chipBuilder,
required this.onChanged,
this.onChipTapped,
this.onSubmitted,
this.onTextChanged,
});
final List<T> values;
final InputDecoration decoration;
final TextStyle? style;
final StrutStyle? strutStyle;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T>? onChipTapped;
final ValueChanged<String>? onSubmitted;
final ValueChanged<String>? onTextChanged;
final Widget Function(BuildContext context, T data) chipBuilder;
@override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>> {
@visibleForTesting
late final ChipsInputEditingController<T> controller;
String _previousText = '';
TextSelection? _previousSelection;
@override
void initState() {
super.initState();
controller = ChipsInputEditingController<T>(<T>[
...widget.values,
], widget.chipBuilder);
controller.addListener(_textListener);
}
@override
void dispose() {
controller.removeListener(_textListener);
controller.dispose();
super.dispose();
}
void _textListener() {
final String currentText = controller.text;
if (_previousSelection != null) {
final int currentNumber = countReplacements(currentText);
final int previousNumber = countReplacements(_previousText);
final int cursorEnd = _previousSelection!.extentOffset;
final int cursorStart = _previousSelection!.baseOffset;
final List<T> values = <T>[...widget.values];
// If the current number and the previous number of replacements are different, then
// the user has deleted the InputChip using the keyboard. In this case, we trigger
// the onChanged callback. We need to be sure also that the current number of
// replacements is different from the input chip to avoid double-deletion.
if (currentNumber < previousNumber && currentNumber != values.length) {
if (cursorStart == cursorEnd) {
values.removeRange(cursorStart - 1, cursorEnd);
} else {
if (cursorStart > cursorEnd) {
values.removeRange(cursorEnd, cursorStart);
} else {
values.removeRange(cursorStart, cursorEnd);
}
}
widget.onChanged(values);
}
}
_previousText = currentText;
_previousSelection = controller.selection;
}
static int countReplacements(String text) {
return text.codeUnits
.where(
(int u) => u == ChipsInputEditingController.kObjectReplacementChar,
)
.length;
}
@override
Widget build(BuildContext context) {
controller.updateValues(<T>[...widget.values]);
return TextField(
minLines: 1,
maxLines: 3,
textInputAction: .done,
style: widget.style,
strutStyle: widget.strutStyle,
controller: controller,
onChanged: (String value) =>
widget.onTextChanged?.call(controller.textWithoutReplacements),
onSubmitted: (String value) =>
widget.onSubmitted?.call(controller.textWithoutReplacements),
);
}
}
class ChipsInputEditingController<T> extends TextEditingController {
ChipsInputEditingController(this.values, this.chipBuilder)
: super(text: String.fromCharCode(kObjectReplacementChar) * values.length);
// This constant character acts as a placeholder in the TextField text value.
// There will be one character for each of the InputChip displayed.
static const int kObjectReplacementChar = 0xFFFE;
List<T> values;
final Widget Function(BuildContext context, T data) chipBuilder;
/// Called whenever chip is either added or removed
/// from the outside the context of the text field.
void updateValues(List<T> values) {
if (values.length != this.values.length) {
final String char = String.fromCharCode(kObjectReplacementChar);
final int length = values.length;
value = TextEditingValue(
text: char * length,
selection: TextSelection.collapsed(offset: length),
);
this.values = values;
}
}
String get textWithoutReplacements {
final String char = String.fromCharCode(kObjectReplacementChar);
return text.replaceAll(RegExp(char), '');
}
String get textWithReplacements => text;
@override
TextSpan buildTextSpan({
required BuildContext context,
TextStyle? style,
required bool withComposing,
}) {
final Iterable<WidgetSpan> chipWidgets = values.map(
(T v) => WidgetSpan(child: chipBuilder(context, v)),
);
return TextSpan(
style: style,
children: <InlineSpan>[
...chipWidgets,
if (textWithoutReplacements.isNotEmpty)
TextSpan(text: textWithoutReplacements),
],
);
}
}
class ToppingSuggestion extends StatelessWidget {
const ToppingSuggestion(this.topping, {super.key, this.onTap});
final String topping;
final ValueChanged<String>? onTap;
@override
Widget build(BuildContext context) {
return ListTile(
key: ObjectKey(topping),
leading: CircleAvatar(child: Text(topping[0].toUpperCase())),
title: Text(topping),
onTap: () => onTap?.call(topping),
);
}
}
class ToppingInputChip extends StatelessWidget {
const ToppingInputChip({
super.key,
required this.topping,
required this.onDeleted,
required this.onSelected,
});
final String topping;
final ValueChanged<String> onDeleted;
final ValueChanged<String> onSelected;
@override
Widget build(BuildContext context) {
return Container(
margin: const .only(right: 3),
child: InputChip(
key: ObjectKey(topping),
label: Text(topping),
avatar: CircleAvatar(child: Text(topping[0].toUpperCase())),
onDeleted: () => onDeleted(topping),
onSelected: (bool value) => onSelected(topping),
materialTapTargetSize: .shrinkWrap,
padding: const .all(2),
),
);
}
}
Material Design 3
InputChip can be used for Input chips from Material Design 3. If ThemeData.useMaterial3 is true, then InputChip will be styled to match the Material Design 3 specification for Input chips.
See also:
- Chip, a chip that displays information and can be deleted.
- ChoiceChip, allows a single selection from a set of options. Choice chips contain related descriptive text or categories.
- FilterChip, uses tags or descriptive words as a way to filter content.
- ActionChip, represents an action related to primary content.
- CircleAvatar, which shows images or initials of people.
- Wrap, A widget that displays its children in multiple horizontal or vertical runs.
- material.io/design/components/chips.html
- Inheritance
-
- Object
- DiagnosticableTree
- Widget
- StatelessWidget
- InputChip
- Implemented types
Constructors
-
InputChip({Key? key, Widget? avatar, required Widget label, TextStyle? labelStyle, EdgeInsetsGeometry? labelPadding, bool selected = false, bool isEnabled = true, ValueChanged<
bool> ? onSelected, Widget? deleteIcon, VoidCallback? onDeleted, Color? deleteIconColor, String? deleteButtonTooltipMessage, VoidCallback? onPressed, double? pressElevation, Color? disabledColor, Color? selectedColor, String? tooltip, BorderSide? side, OutlinedBorder? shape, Clip clipBehavior = Clip.none, FocusNode? focusNode, bool autofocus = false, WidgetStateProperty<Color?> ? color, Color? backgroundColor, EdgeInsetsGeometry? padding, VisualDensity? visualDensity, MaterialTapTargetSize? materialTapTargetSize, double? elevation, Color? shadowColor, Color? surfaceTintColor, IconThemeData? iconTheme, Color? selectedShadowColor, bool? showCheckmark, Color? checkmarkColor, ShapeBorder avatarBorder = const CircleBorder(), BoxConstraints? avatarBoxConstraints, BoxConstraints? deleteIconBoxConstraints, ChipAnimationStyle? chipAnimationStyle, MouseCursor? mouseCursor}) -
Creates an InputChip.
const
Properties
- autofocus → bool
-
True if this widget will be selected as the initial focus when no other
node in its scope is currently focused.
final
- avatar → Widget?
-
A widget to display prior to the chip's label.
final
- avatarBorder → ShapeBorder
-
The shape of the translucent highlight painted over the avatar when the
selected property is true.
final
- avatarBoxConstraints → BoxConstraints?
-
Optional size constraints for the avatar.
final
- backgroundColor → Color?
-
Color to be used for the unselected, enabled chip's background.
final
- checkmarkColor → Color?
-
Color of the chip's check mark when a check mark is visible.
final
- chipAnimationStyle → ChipAnimationStyle?
-
Used to override the default chip animations durations.
final
- clipBehavior → Clip
-
The content will be clipped (or not) according to this option.
final
-
color
→ WidgetStateProperty<
Color?> ? -
The color that fills the chip, in all WidgetStates.
final
- deleteButtonTooltipMessage → String?
-
The message to be used for the chip's delete button tooltip.
final
- deleteIcon → Widget?
-
The icon displayed when onDeleted is set.
final
- deleteIconBoxConstraints → BoxConstraints?
-
Optional size constraints for the delete icon.
final
- deleteIconColor → Color?
-
Used to define the delete icon's color with an IconTheme that
contains the icon.
final
- disabledColor → Color?
-
The color used for the chip's background to indicate that it is not
enabled.
final
- elevation → double?
-
Elevation to be applied on the chip relative to its parent.
final
- focusNode → FocusNode?
-
An optional focus node to use as the focus node for this widget.
final
- hashCode → int
-
The hash code for this object.
no setterinherited
- iconTheme → IconThemeData?
-
Theme used for all icons in the chip.
final
- isEnabled → bool
-
Whether or not this chip is enabled for input.
final
- key → Key?
-
Controls how one widget replaces another widget in the tree.
finalinherited
- label → Widget
-
The primary content of the chip.
final
- labelPadding → EdgeInsetsGeometry?
-
The padding around the label widget.
final
- labelStyle → TextStyle?
-
The style to be applied to the chip's label.
final
- materialTapTargetSize → MaterialTapTargetSize?
-
Configures the minimum size of the tap target.
final
- mouseCursor → MouseCursor?
-
The cursor for a mouse pointer when it enters or is hovering over the
widget.
final
- onDeleted → VoidCallback?
-
Called when the user taps the deleteIcon to delete the chip.
final
- onPressed → VoidCallback?
-
Called when the user taps the chip.
final
-
onSelected
→ ValueChanged<
bool> ? -
Called when the chip should change between selected and de-selected
states.
final
- padding → EdgeInsetsGeometry?
-
The padding between the contents of the chip and the outside shape.
final
- pressElevation → double?
-
Elevation to be applied on the chip relative to its parent during the
press motion.
final
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
- selected → bool
-
Whether or not this chip is selected.
final
- selectedColor → Color?
-
Color to be used for the chip's background, indicating that it is
selected.
final
- selectedShadowColor → Color?
-
Color of the chip's shadow when the elevation is greater than 0 and the
chip is selected.
final
- shadowColor → Color?
-
Color of the chip's shadow when the elevation is greater than 0.
final
- shape → OutlinedBorder?
-
The OutlinedBorder to draw around the chip.
final
- showCheckmark → bool?
-
Whether or not to show a check mark when
SelectableChipAttributes.selected is true.
final
- side → BorderSide?
-
The color and weight of the chip's outline.
final
- surfaceTintColor → Color?
-
Color of the chip's surface tint overlay when its elevation is
greater than 0.
final
- tooltip → String?
-
Tooltip string to be used for the body area (where the label and avatar
are) of the chip.
final
- visualDensity → VisualDensity?
-
Defines how compact the chip's layout will be.
final
Methods
-
build(
BuildContext context) → Widget -
Describes the part of the user interface represented by this widget.
override
-
createElement(
) → StatelessElement -
Creates a StatelessElement to manage this widget's location in the tree.
inherited
-
debugDescribeChildren(
) → List< DiagnosticsNode> -
Returns a list of DiagnosticsNode objects describing this node's
children.
inherited
-
debugFillProperties(
DiagnosticPropertiesBuilder properties) → void -
Add additional properties associated with the node.
inherited
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
toDiagnosticsNode(
{String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode -
Returns a debug representation of the object that is used by debugging
tools and by DiagnosticsNode.toStringDeep.
inherited
-
toString(
{DiagnosticLevel minLevel = DiagnosticLevel.info}) → String -
A string representation of this object.
inherited
-
toStringDeep(
{String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) → String -
Returns a string representation of this node and its descendants.
inherited
-
toStringShallow(
{String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String -
Returns a one-line detailed description of the object.
inherited
-
toStringShort(
) → String -
A short, textual description of this widget.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited