Skip to main content

Handling user input

Now that you know how to manage state in your Flutter app, how can you let users interact with your app and change its state?

Introduction to handling user input

#

As a multi-platform UI framework, there are many different ways for users to interact with a Flutter app. The resources in this section introduce you to some of the common widgets used for enabling user interaction within your app.

Some user input mechanisms, like scrolling, have already been covered in Layouts.

Reference: The widget catalog has an inventory of commonly used widgets in the Material and Cupertino libraries.

Next, we'll cover a few of the Material widgets that support common use cases for handling user input in your Flutter app.

Buttons

#

A collection of Material 3 Buttons.

Buttons allow a user to initiate an action in the UI by clicking or tapping. The Material library provides a variety of button types that are functionally similar, but styled differently for various use cases, including:

  • ElevatedButton: A button with some depth. Use elevated buttons to add dimension to otherwise mostly flat layouts.
  • FilledButton: A filled button that should be used for important, final actions that complete a flow, like Save, Join now, or Confirm.
  • Tonal Button: A middle ground button between FilledButton and OutlinedButton. They're useful in contexts where a lower-priority button requires more emphasis than an outline, like Next.
  • OutlinedButton: A button with text and a visible border. These buttons contain actions that are important, but aren't the primary action in an app.
  • TextButton: Clickable text, without a border. Since text buttons don't have visible borders, they must rely on their position relative to other content for context.
  • IconButton: A button with an icon.
  • FloatingActionButton: An icon button that hovers over content to promote a primary action.

Video: FloatingActionButton (Widget of the Week)

There are usually 3 main aspects to constructing a button: style, callback, and its child, as seen in the following ElevatedButton sample code:

  • A button's callback function, onPressed, determines what happens when the button is clicked, therefore, this function is where you update your app state. If the callback is null, the button is disabled and nothing happens when a user presses the button.

  • The button's child, which is displayed within the button's content area, is usually text or an icon that indicates the button's purpose.

  • Finally, a button's style controls its appearance: color, border, and so on.

dart
int count = 0;

@override
Widget build(BuildContext context) {
  return ElevatedButton(
    style: ElevatedButton.styleFrom(
      textStyle: const TextStyle(fontSize: 20),
    ),
    onPressed: () {
      setState(() {
        count += 1;
      });
    },
    child: const Text('Enabled'),
  );
}

A GIF of an elevated button with the text "Enabled"
This figure shows an ElevatedButton with the text "Enabled" being clicked.


Checkpoint: Complete this tutorial that teaches you how to build a "favorite" button: Add interactivity to your Flutter app


API Docs: ElevatedButtonFilledButtonOutlinedButtonTextButtonIconButtonFloatingActionButton

Text

#

Several widgets support text input.

SelectableText

#

Flutter's Text widget displays text on the screen, but doesn't allow users to highlight or copy the text. SelectableText displays a string of user-selectable text.

dart
@override
Widget build(BuildContext context) {
  return const SelectableText('''
Two households, both alike in dignity,
In fair Verona, where we lay our scene,
From ancient grudge break to new mutiny,
Where civil blood makes civil hands unclean.
From forth the fatal loins of these two foes''');
}

A GIF of a cursor highlighting two lines of text from a paragraph.
This figure shows a cursor highlighting a portion of a string of text.

Video: SelectableText (Widget of the Week)

RichText

#

RichText lets you display strings of rich text in your app. TextSpan, similar to RichText, allows you to display parts of text with different text styles. It's not for handling user input, but is useful if you're allowing users edit and format text.

dart
@override
Widget build(BuildContext context) {
  return RichText(
    text: TextSpan(
      text: 'Hello ',
      style: DefaultTextStyle.of(context).style,
      children: const <TextSpan>[
        TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
        TextSpan(text: ' world!'),
      ],
    ),
  );
}

A screenshot of the text "Hello bold world!" with the word "bold" in bold font.
This figure shows a string of text formatted with different text styles.

Video: Rich Text (Widget of the Week)

Demo: Rich Text Editor

Code: Rich Text Editor code

TextField

#

A TextField lets users enter text in text box using a hardware or onscreen keyboard.

TextFields have many different properties and configurations. A few of the highlights:

  • InputDecoration determines the text field's appearance, such as color and border.
  • controller: A TextEditingController controls the text being edited. Why might you need a controller? By default, your app's users can type into the text field, but if you want to programmatically control the TextField and clear its value, for example, you'll need a TextEditingController.
  • onChanged: This callback function triggers when the user changes the text field's value, such as when inserting or removing text.
  • onSubmitted: This callback is triggered when the user indicates that they are done editing the text in the field; for example, by tapping the "enter" key when the text field is in focus.

The class supports other configurable properties, such as obscureText that turns each letter into a readOnly circle as its entered and readOnly which prevents the user from changing the text.

dart
final TextEditingController _controller = TextEditingController();

@override
Widget build(BuildContext context) {
  return TextField(
    controller: _controller,
    decoration: const InputDecoration(
      border: OutlineInputBorder(),
      labelText: 'Mascot Name',
    ),
  );
}

A GIF of a text field with the label "Mascot Name", purple focus border and the phrase "Dash the hummingbird" being typed in.
This figure shows text being typed into a TextField with a selected border and label.

Checkpoint: Complete this 4-part cookbook series that walks you through how to create a text field, retrieve its value, and update your app state:

  1. Create and style a text field
  2. Retrieve the value of a text field
  3. Handle changes to a text field
  4. Focus and text fields.

Form

#

Form is an optional container for grouping together multiple form field widgets, such as TextField.

Each individual form field should be wrapped in a FormField widget with the Form widget as a common ancestor. Convenience widgets exist that pre-wrap form field widgets in a FormField for you. For example, the Form widget version of TextField is TextFormField.

Using a Form provides access to a FormState, which lets you save, reset, and validate each FormField that descends from this Form. You can also provide a GlobalKey to identify a specific form, as shown in the following code:

dart
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {
  return Form(
    key: _formKey,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        TextFormField(
          decoration: const InputDecoration(
            hintText: 'Enter your email',
          ),
          validator: (String? value) {
            if (value == null || value.isEmpty) {
              return 'Please enter some text';
            }
            return null;
          },
        ),
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 16.0),
          child: ElevatedButton(
            onPressed: () {
              // Validate returns true if the form is valid, or false otherwise.
              if (_formKey.currentState!.validate()) {
                // Process data.
              }
            },
            child: const Text('Submit'),
          ),
        ),
      ],
    ),
  );
}

Checkpoint: Complete this tutorial to learn how to build a form with validation.

Demo: Form app

Code: Form app code


API Docs: TextFieldRichTextSelectableTextForm

Select a value from a group of options

#

Provide a way to users to select from several options.

SegmentedButton

#

SegmentedButton allows users to select from a minimal group of 2-5 items.

The data type, <T>, can be a built-in type such as int, String, bool or an enum. A SegmentedButton has a few relevant properties:

  • segments, a list of ButtonSegments, where each represents a "segment" or option that the user can select. Visually, each ButtonSegment can have an icon, text label, or both.

  • multiSelectionEnabled indicates whether the user is allowed to select multiple options. This property defaults to false.

  • selected identifies the currently selected value(s). Note: selected is of type of Set<T>, so if you're only allowing users to select one value, that value must be provided as aSet with a single element.

  • The onSelectionChanged callback triggers when a user selects any segments. It provides a list of the selected segments so you can update your app state.

  • Additional styling parameters allow you to modify the button's appearance. For example, style takes a ButtonStyle, providing a way to configure a selectedIcon.

dart
enum Calendar { day, week, month, year }

// StatefulWidget...
Calendar calendarView = Calendar.day;

@override
Widget build(BuildContext context) {
  return SegmentedButton<Calendar>(
    segments: const <ButtonSegment<Calendar>>[
      ButtonSegment<Calendar>(
          value: Calendar.day,
          label: Text('Day'),
          icon: Icon(Icons.calendar_view_day)),
      ButtonSegment<Calendar>(
          value: Calendar.week,
          label: Text('Week'),
          icon: Icon(Icons.calendar_view_week)),
      ButtonSegment<Calendar>(
          value: Calendar.month,
          label: Text('Month'),
          icon: Icon(Icons.calendar_view_month)),
      ButtonSegment<Calendar>(
          value: Calendar.year,
          label: Text('Year'),
          icon: Icon(Icons.calendar_today)),
    ],
    selected: <Calendar>{calendarView},
    onSelectionChanged: (Set<Calendar> newSelection) {
      setState(() {
        Suggested change
        // By default there is only a single segment that can be
        // selected at one time, so its value is always the first
        // By default, only a single segment can be
        // selected at one time, so its value is always the first
        calendarView = newSelection.first;
      });
    },
  );
}

A GIF of a SegmentedButton with 4 segments: Day, Week, Month, and Year.
Each has a calendar icon to represent its value and a text label.
Day is first selected, then week and month, then year.
This figure shows a SegmentedButton, each segment with an icon and text representing its value.

Chip

#

Chip is a compact way of representing an attribute, text, entity, or action for a specific context. Specialized Chip widgets exist for specific use cases:

  • InputChip represents a complex piece of information, such as an entity (person, place, or thing), or conversational text, in a compact form.
  • ChoiceChip allows a single selection from a set of options. Choice chips contain related descriptive text or categories.
  • FilterChip uses tags or descriptive words to filter content.
  • ActionChip represents an action related to primary content.

Every Chip widget requires a label. It can optionally have an avatar (such as an icon or a user's profile picture) and an onDeleted callback, which shows a delete icon that when triggered, deletes the chip. A Chip widget's appearance can also be customized by setting a number of optional parameters such as shape, color, and iconTheme.

You will typically use Wrap, a widget that displays its children in multiple horizontal or vertical runs, to make sure your chips wrap and don't get cut off at the edge of your app.

dart
@override
Widget build(BuildContext context) {
  return const SizedBox(
    width: 500,
    child: Wrap(
      alignment: WrapAlignment.center,
      spacing: 8,
      runSpacing: 4,
      children: [
        Chip(
          avatar: CircleAvatar(
              backgroundImage: AssetImage('assets/images/dash_chef.png')),
          label: Text('Chef Dash'),
        ),
        Chip(
          avatar: CircleAvatar(
              backgroundImage:
                  AssetImage('assets/images/dash_firefighter.png')),
          label: Text('Firefighter Dash'),
        ),
        Chip(
          avatar: CircleAvatar(
              backgroundImage: AssetImage('assets/images/dash_musician.png')),
          label: Text('Musician Dash'),
        ),
        Chip(
          avatar: CircleAvatar(
              backgroundImage: AssetImage('assets/images/dash_artist.png')),
          label: Text('Artist Dash'),
        ),
      ],
    ),
  );
}

A screenshot of 4 Chips split over two rows with a leading circular
profile image with content text.
This figure shows two rows of Chip widgets, each containing a circular leading profile image and content text.

#

A DropdownMenu allows users to select a choice from a menu of options and places the selected text into a TextField. It also allows users to filter the menu items based on the text input.

Configuration parameters include the following:

  • dropdownMenuEntries provides a list of DropdownMenuEntrys that describes each menu item. The menu might contain information such as a text label, and a leading or trailing icon. (This is also the only required parameter.)
  • TextEditingController allows programmatically controlling the TextField.
  • The onSelected callback triggers when the user selects an option.
  • initialSelection allows you to configure the default value.
  • Additional parameters are also available for customizing the widget's look and behavior.
dart
enum ColorLabel {
  blue('Blue', Colors.blue),
  pink('Pink', Colors.pink),
  green('Green', Colors.green),
  yellow('Orange', Colors.orange),
  grey('Grey', Colors.grey);

  const ColorLabel(this.label, this.color);
  final String label;
  final Color color;
}

// StatefulWidget...
@override
Widget build(BuildContext context) {
  return DropdownMenu<ColorLabel>(
    initialSelection: ColorLabel.green,
    controller: colorController,
    // requestFocusOnTap is enabled/disabled by platforms when it is null.
    // On mobile platforms, this is false by default. Setting this to true will
    // trigger focus request on the text field and virtual keyboard will appear
    // afterward. On desktop platforms however, this defaults to true.
    requestFocusOnTap: true,
    label: const Text('Color'),
    onSelected: (ColorLabel? color) {
      setState(() {
        selectedColor = color;
      });
    },
    dropdownMenuEntries: ColorLabel.values
      .map<DropdownMenuEntry<ColorLabel>>(
          (ColorLabel color) {
            return DropdownMenuEntry<ColorLabel>(
              value: color,
              label: color.label,
              enabled: color.label != 'Grey',
              style: MenuItemButton.styleFrom(
                foregroundColor: color.color,
              ),
            );
      }).toList(),
  );
}

A GIF the DropdownMenu widget that is selected, it displays 5 options:
Blue, Pink, Green, Orange, and Grey. The option text is displayed in the color
of its value.
This figure shows a DropdownMenu widget with 5 value options. Each option's text color is styled to represent the color value.

Video: DropdownMenu (Widget of the Week)

Slider

#

The Slider widget lets a user adjust a value by moving an indicator, such as a volume bar.

Configuration parameters for the Slider widget:

  • value represents the slider's current value
  • onChanged is the callback that gets triggered when the handle is moved
  • min and max establish minimum and maximum values allowed by the slider
  • divisions establishes a discrete interval with which the user can move the handle along the track.
dart
double _currentVolume = 1;

@override
Widget build(BuildContext context) {
  return Slider(
    value: _currentVolume,
    max: 5,
    divisions: 5,
    label: _currentVolume.toString(),
    onChanged: (double value) {
      setState(() {
        _currentVolume = value;
      });
    },
  );
}

A GIF of a slider that has the dial dragged left to right in increments 
of 1, from 0.0 to 5.0
This figure shows a slider widget with a value ranging from 0.0 to 5.0 broken up into 5 divisions. It shows the current value as a label as the dial is dragged.

Video: Slider, RangeSlider, CupertinoSlider (Widget of the Week)


API Docs: SegmentedButtonDropdownMenuSliderChip

Toggle between values

#

There are several ways that your UI can allow toggling between values.

Checkbox, Switch, and Radio

#

Provide an option to toggle a single value on and off. The functional logic behind these widgets are the same, as all 3 are built on top of ToggleableStateMixin, though each provides slight presentation differences.:

  • Checkbox is a container that is empty when false or filled with a checkmark when true.
  • Switch has a handle that is on the left when false and slides to the right when true.
  • Radio is similar to a Checkbox in that it's a container that is empty when false, but filled in when true.

The configuration for Checkbox and Switch contain:

  • a value that is true or false
  • and an onChanged callback which is triggered when the user toggles the widget

Checkbox

#
dart
bool isChecked = false;

@override
Widget build(BuildContext context) {
  return Checkbox(
    checkColor: Colors.white,
    value: isChecked,
    onChanged: (bool? value) {
      setState(() {
        isChecked = value!;
      });
    },
  );
}

A GIF that shows a pointer clicking a checkbox 
and then clicking again to uncheck it.
This figure shows a checkbox being checked and unchecked.

Switch

#
dart
bool light = true;

@override
Widget build(BuildContext context) {
  return Switch(
    // This bool value toggles the switch.
    value: light,
    activeColor: Colors.red,
    onChanged: (bool value) {
      // This is called when the user toggles the switch.
      setState(() {
        light = value;
      });
    },
  );
}

A GIF of a Switch widget that is toggled on and off. In its off state,
it is gray with dark gray borders. In its on state, 
it is red with a light red border.
This figure shows a Switch widget that is toggled on and off.

Radio

#

A group of Radio buttons that allows the user to select between mutually exclusive values. When the user selects a radio button in a group, the other radio buttons are unselected.

  • A particular Radio button's value represent that button's value,
  • The selected value for a group of radio buttons is identified by the groupValue parameter.
  • Radio also has an onChanged callback that gets triggered when users click it, like Switch and Checkbox
dart
enum Character { musician, chef, firefighter, artist }

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

  @override
  State<RadioExample> createState() => _RadioExampleState();
}

class _RadioExampleState extends State<RadioExample> {
  Character? _character = Character.musician;

  void setCharacter(Character? value) {
    setState(() {
      _character = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ListTile(
          title: const Text('Musician'),
          leading: Radio<Character>(
            value: Character.musician,
            groupValue: _character,
            onChanged: setCharacter,
          ),
        ),
        ListTile(
          title: const Text('Chef'),
          leading: Radio<Character>(
            value: Character.chef,
            groupValue: _character,
            onChanged: setCharacter,
          ),
        ),
        ListTile(
          title: const Text('Firefighter'),
          leading: Radio<Character>(
            value: Character.firefighter,
            groupValue: _character,
            onChanged: setCharacter,
          ),
        ),
        ListTile(
          title: const Text('Artist'),
          leading: Radio<Character>(
            value: Character.artist,
            groupValue: _character,
            onChanged: setCharacter,
          ),
        ),
      ],
    );
  }
}

A GIF of 4 ListTiles in a column, each containing a leading Radio button
and title text. The Radio buttons are selected in order from top to bottom.
This figure shows a column of ListTiles containing a radio button and label, where only one radio button can be selected at a time.

Bonus: CheckboxListTile & SwitchListTile

#

These convenience widgets are the same checkbox and switch widgets, but support a label (as a ListTile).

dart
double timeDilation = 1.0;
bool _lights = false;

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      CheckboxListTile(
        title: const Text('Animate Slowly'),
        value: timeDilation != 1.0,
        onChanged: (bool? value) {
          setState(() {
            timeDilation = value! ? 10.0 : 1.0;
          });
        },
        secondary: const Icon(Icons.hourglass_empty),
      ),
      SwitchListTile(
        title: const Text('Lights'),
        value: _lights,
        onChanged: (bool value) {
          setState(() {
            _lights = value;
          });
        },
        secondary: const Icon(Icons.lightbulb_outline),
      ),
    ],
  );
}

A ListTile with a leading icon, title text, and a trailing checkbox being
checked and unchecked. It also shows a ListTile with a leading icon, title text
and a switch being toggled on and off.
This figure shows a column containing a CheckboxListTile and a SwitchListTile being toggled.

Video: CheckboxListTile (Widget of the Week)

Video: SwitchListTile (Widget of the Week)


API Docs: CheckboxCheckboxListTileSwitchSwitchListTileRadio

Select a date or time

#

Widgets are provided so the user can select a date and time.

There is a set of dialogs that enable users to select a date or time, as you'll see in the following sections. With the exception of differing date types - DateTime for dates vs TimeOfDay for time - these dialogs function similarly, you can configure them by providing:

  • a default initialDate or initialTime
  • or an initialEntryMode that determines the picker UI that's displayed.

DatePickerDialog

#

This dialog allows the user to select a date or a range of dates. Activate by calling the showDatePicker function, which returns a Future<DateTime>, so don't forget to await the asynchronous function call!

dart
DateTime? selectedDate;

@override
Widget build(BuildContext context) {
  var date = selectedDate;

  return Column(children: [
    Text(
      date == null
          ? "You haven't picked a date yet."
          : DateFormat('MM-dd-yyyy').format(date),
    ),
    ElevatedButton.icon(
      icon: const Icon(Icons.calendar_today),
      onPressed: () async {
        var pickedDate = await showDatePicker(
          context: context,
          initialEntryMode: DatePickerEntryMode.calendarOnly,
          initialDate: DateTime.now(),
          firstDate: DateTime(2019),
          lastDate: DateTime(2050),
        );

        setState(() {
          selectedDate = pickedDate;
        });
      },
      label: const Text('Pick a date'),
    )
  ]);
}

A GIF of a pointer clicking a button that says "Pick a date",
then shows a date picker. The date Friday, August 30 is selected and the "OK"
button is clicked.
This figure shows a DatePicker that is displayed when the "Pick a date" button is clicked.

TimePickerDialog

#

TimePickerDialog is a dialog that presents a time picker. It can be activated by calling the showTimePicker() function. Instead of returning a Future<DateTime>, showTimePicker instead returns a Future<TimeOfDay>. Once again, don't forget to await the function call!

dart
TimeOfDay? selectedTime;

@override
Widget build(BuildContext context) {
  var time = selectedTime;

  return Column(children: [
    Text(
      time == null ? "You haven't picked a time yet." : time.format(context),
    ),
    ElevatedButton.icon(
      icon: const Icon(Icons.calendar_today),
      onPressed: () async {
        var pickedTime = await showTimePicker(
          context: context,
          initialEntryMode: TimePickerEntryMode.dial,
          initialTime: TimeOfDay.now(),
        );

        setState(() {
          selectedTime = pickedTime;
        });
      },
      label: const Text('Pick a date'),
    )
  ]);
}

A GIF of a pointer clicking a button that says "Pick a time", then shows
 a time picker. The time picker shows a circular clock as the cursor moves the 
 hour hand, then minute hand, selects PM, then the "OK" button is clicked.
This figure shows a TimePicker that is displayed when the "Pick a time" button is clicked.


API Docs: showDatePickershowTimePicker

Swipe & slide

#

A Dismissible is a widget that enables users to dismiss it by swiping. It has a number of configuration parameters, including:

  • A child widget
  • An onDismissed callback that is triggered when the user swipes
  • Styling parameters such as background
  • It's important to include a key object as well so that they can be uniquely identified from sibling Dismissible widgets in the widget tree.
dart
List<int> items = List<int>.generate(100, (int index) => index);

@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    padding: const EdgeInsets.symmetric(vertical: 16),
    itemBuilder: (BuildContext context, int index) {
      return Dismissible(
        background: Container(
          color: Colors.green,
        ),
        key: ValueKey<int>(items[index]),
        onDismissed: (DismissDirection direction) {
          setState(() {
            items.removeAt(index);
          });
        },
        child: ListTile(
          title: Text(
            'Item ${items[index]}',
          ),
        ),
      );
    },
  );
}

A screenshot of three widgets, spaced evenly from each other.
This figure shows a list of Dismissible widgets that each contain a ListTile. Swiping across the ListTile reveals a green background makes the tile disappear.

Video: Dismissible (Widget of the Week)

Checkpoint: Complete this tutorial on how to implement swipe to dismiss using the dismissible widget.


API Docs: Dismissible

Looking for more widgets?

#

This page features just a few of the common Material widgets that you can use for handling user input in your Flutter app. Check out the Material Widget library and Material Library API docs for a full list of widgets.

Demo: See Flutter's Material 3 Demo for a curated sample of user input widgets available in the Material library.

If the Material and Cupertino libraries don't have a widget that does what you need, check out pub.dev to find Flutter & Dart community-owned and maintained packages. For example, the flutter_slidable package provides a Slidable widget that is more customizable than the Dismissible widget described in the previous section.

Video: flutter_slidable (Package of the Week)

Build interactive widgets with GestureDetector

#

Have you scoured the widget libraries, pub.dev, asked your coding friends, and still can't find a widget that fits the user interaction that you're looking for? You can build your own custom widget and make it interactive using GestureDetector.

Checkpoint: Use this recipe as a starting point to create your own custom button widget that can handle taps.

Video: GestureDetector (Widget of the Week)

Reference: Check out Taps, drags, and other gestures which explains how to listen for, and respond to, gestures in Flutter.

Bonus Video: Curious how Flutter's GestureArena turns raw user interaction data into human recognizable concepts like taps, drags, and pinches? Check out this video: GestureArena (Decoding Flutter)

Don't forget about accessibility!

#

If you're building a custom widget, annotate its meaning with the Semantics widget. It provides descriptions and metadata to screen readers and other semantic analysis-based tools.

Video: Semantics (Flutter Widget of the Week)


API Docs: GestureDetectorSemantics

Testing

#

Once you have finished building user interactions into your app, don't forget to write tests to ensure that everything works as expected!

These tutorials walk you through writing tests that simulate user interactions in your app:

Checkpoint: Follow this tap, drag, and enter text cookbook article and learn how to use WidgetTester to simulate and test user interactions in your app.

Bonus Tutorial: The handle scrolling cookbook recipe shows you how to verify that lists of widgets contain the expected content by scrolling through the lists using widget tests.

Next: Networking

#

This page was an introduction to handling user input. Now that you know how to handle input from app users, you can make your app even more interesting by adding external data. In the next section, you'll learn now to fetch data for your app over a network, how to convert data to and from JSON, authentication, and other networking features.

Feedback

#

As this section of the website is evolving, we welcome your feedback!