A field that suggests as you type is one of those features that feels fiddly until you learn that Flutter ships two ready-made answers. The Autocomplete widget handles inline completion from a list, and SearchAnchor with SearchBar gives you a full Material 3 search experience. This tutorial walks through Autocomplete, filtering with optionsBuilder, customising the field with fieldViewBuilder and the dropdown with optionsViewBuilder, debouncing an async API search, and the Material 3 SearchAnchor, all with paste-ready code.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowEvery snippet below runs against current stable Flutter with no extra packages, except the API example, which uses the http package you may already have.
We'll start with the simplest possible autocomplete, then layer on filtering, custom styling, and an async source.
If you'd rather watch a live search field wired to a real REST API, the channel builds these flows end to end.
Autocomplete in one widget
The Autocomplete<T> widget bundles a text field and a suggestions overlay. The two callbacks you must provide are optionsBuilder, which returns the matching options for the current text, and onSelected, which fires when the user taps one. For a fixed list of strings, the type argument is String and that is all you need.
Autocomplete<String>(
optionsBuilder: (TextEditingValue value) {
if (value.text.isEmpty) return const Iterable<String>.empty();
return _fruits.where(
(f) => f.toLowerCase().contains(value.text.toLowerCase()),
);
},
onSelected: (selection) => debugPrint('Picked: $selection'),
)
That is a working autocomplete: type a few letters and Flutter shows the matching fruits in a floating list. Note the empty-text guard — returning an empty Iterable keeps the dropdown closed until the user actually types something.
The type argument <String> tells Flutter what each option is. For a plain list of labels that is all you need, and the selected string is written straight back into the field. The widget also handles the keyboard for you: arrow keys move the highlight and Enter selects the highlighted option, so the field is accessible without any extra work on your part.
optionsBuilder: filter as you type
All your filtering logic lives in optionsBuilder. It receives a TextEditingValue on every keystroke, and whatever Iterable you return becomes the visible suggestions. Lower-case both sides so matching is case-insensitive, and decide whether you want contains (matches anywhere) or startsWith (prefix only).
optionsBuilder: (TextEditingValue value) {
final query = value.text.trim().toLowerCase();
if (query.isEmpty) return const Iterable<String>.empty();
return _cities.where((c) => c.toLowerCase().startsWith(query));
},
One decision worth making early is contains versus startsWith. contains matches the query anywhere in the option, which is forgiving and good for free-text search; startsWith matches only the beginning, which feels more precise for things like country or airport codes. There is no right answer — pick the one that matches how users think about your data, and stay consistent across the app.
If your options are objects rather than strings, supply displayStringForOption so the widget knows what text to put in the field after selection. That keeps your model intact while showing a friendly label.
Autocomplete<City>(
displayStringForOption: (city) => city.name,
optionsBuilder: (value) => _cities.where(
(c) => c.name.toLowerCase().contains(value.text.toLowerCase()),
),
onSelected: (city) => _select(city),
)

Build search and form UIs that feel native
The Complete Flutter Guide covers autocomplete, search, REST APIs, and state inside real, shipped apps.
Enrol nowCustomise the field with fieldViewBuilder
By default Autocomplete renders a plain TextField. To style it — add a label, an icon, a border — supply fieldViewBuilder. Flutter hands you a ready-made controller, focus node, and an onFieldSubmitted callback; wire them into your own field and customise the rest. This builds directly on TextField and its controller.
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
return TextField(
controller: controller,
focusNode: focusNode,
decoration: const InputDecoration(
labelText: 'Search cities',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onSubmitted: (_) => onFieldSubmitted(),
);
},
You must pass the supplied controller and focusNode through unchanged — the widget relies on them to track the query and manage the overlay. Calling onFieldSubmitted on submit keeps keyboard selection working. A common slip is to create a fresh TextEditingController here out of habit; do not, because Autocomplete already owns one and will not see your typing if you swap it out.
This is the seam where autocomplete meets ordinary form work. Because you are handed a real TextField underneath, every decoration trick you know — helper text, validation borders, suffix icons, a clear button — applies unchanged. You can also wrap the whole thing in a Form and validate it like any other field, since the selected value lives in the controller you were given.
Customise the dropdown with optionsViewBuilder
To control how each suggestion looks — two-line tiles, leading avatars, custom colours — provide optionsViewBuilder. You receive the onSelected callback and the current options, and you return the overlay yourself. Wrap it in Material so it gets elevation and the right background.
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4,
child: SizedBox(
width: 300,
child: ListView(
shrinkWrap: true,
children: options
.map((opt) => ListTile(
leading: const Icon(Icons.place_outlined),
title: Text(opt.toString()),
onTap: () => onSelected(opt),
))
.toList(),
),
),
),
);
},
Call onSelected(opt) in each tile's onTap — that is what tells Autocomplete a choice was made and closes the overlay. Render the list with a ListView so long result sets stay efficient.
Search an API (debounced async)
Real search usually hits a server. optionsBuilder can be async and return a Future<Iterable>, but firing a request on every keystroke is wasteful. The fix is to debounce: cancel any pending timer, start a new one, and only call the network once the user pauses.
Timer? _debounce;
Future<Iterable<String>> _search(String query) {
_debounce?.cancel();
final completer = Completer<Iterable<String>>();
_debounce = Timer(const Duration(milliseconds: 350), () async {
final res = await http.get(
Uri.parse('https://api.example.com/search?q=$query'),
);
final data = jsonDecode(res.body) as List;
completer.complete(data.map((e) => e['name'] as String));
});
return completer.future;
}
Then call it from the builder, guarding the empty case so you do not query the server for nothing.
optionsBuilder: (value) async {
if (value.text.trim().isEmpty) {
return const Iterable<String>.empty();
}
return _search(value.text.trim());
},
Remember to cancel the timer in dispose, and if you touch any widget state after the await, guard with a mounted check first — the field could be gone by the time the response lands.
Two refinements make an async search feel professional. First, show a loading hint while a request is in flight, so the user knows something is happening rather than staring at an empty dropdown. Second, ignore stale responses: if the user has typed more since a request was sent, a slow earlier response can arrive last and overwrite the newer results. Debouncing reduces how often this happens, but for a busy search you may also want to track the latest query and discard responses that no longer match it. For anything more elaborate — caching results or cancelling in-flight requests — graduate to the patterns in a fuller HTTP setup.
SearchAnchor and SearchBar (Material 3)
For a full search surface rather than inline completion, Material 3 offers SearchAnchor. It pairs a trigger — usually a SearchBar — with a search view that expands above the page. The suggestionsBuilder returns the result widgets as the user types, and the controller lets you read the query and close the view.
SearchAnchor(
builder: (context, controller) {
return SearchBar(
controller: controller,
hintText: 'Search products',
leading: const Icon(Icons.search),
onTap: controller.openView,
onChanged: (_) => controller.openView(),
);
},
suggestionsBuilder: (context, controller) {
final query = controller.text.toLowerCase();
return _products
.where((p) => p.toLowerCase().contains(query))
.map((p) => ListTile(
title: Text(p),
onTap: () {
controller.closeView(p);
},
));
},
)
Calling controller.closeView(p) dismisses the search view and writes the chosen value back into the bar. Because SearchBar and the view are themed by Material 3, they pick up your colour scheme automatically — reach for SearchAnchor when you want a polished, app-wide search rather than a single completing field.
The mental model is worth holding onto. Autocomplete is a single field that completes inline, ideal for a form input where the user is choosing one value. SearchAnchor is a whole search surface that takes over the screen, ideal for the search icon in an app bar that opens into a results page. suggestionsBuilder may be async and return a Future, so the same debouncing advice applies when it is backed by an API. Choosing the right one is usually obvious once you ask whether you are completing a field or running a search.
Common mistakes
- No empty-text guard. Return an empty
Iterablewhen the query is blank, or the dropdown opens with the entire list. - Forgetting case-insensitive matching. Lower-case both the query and each option in
optionsBuilder, or matches feel broken. - Swapping out the supplied controller. In
fieldViewBuilderyou must reuse the controller and focus node Flutter gives you, not create your own. - Querying the API every keystroke. Debounce with a
Timerso you fetch only when the user pauses, and cancel it indispose. - Not calling onSelected in a custom dropdown. Each tile in
optionsViewBuildermust callonSelected, or taps do nothing.
Frequently asked questions
How do I add autocomplete in Flutter?
Use the built-in Autocomplete widget. Give it an optionsBuilder that returns the matching options for the current text and an onSelected callback for taps; Flutter renders the field and the suggestions overlay for you, so a basic autocomplete needs no extra packages.
How do I filter the suggestions?
Filtering lives inside optionsBuilder. Read textEditingValue.text, lower-case it, and return only the items whose label contains the query. Return an empty iterable when the field is empty so the dropdown stays closed.
How do I debounce an API-backed search?
Keep a Timer, cancel it on every keystroke, and fire the request only after the user pauses for a few hundred milliseconds. Make optionsBuilder async, await the debounced fetch, and guard with a mounted check after the await.
What is SearchAnchor in Flutter?
SearchAnchor is the Material 3 search component. It pairs a SearchBar with a full search view and a suggestionsBuilder that returns results as the user types — the modern, themed way to add search, whereas Autocomplete suits inline field completion.
Further reads
Keep going with the tutorials that pair with this guide:
- Flutter Development Guide 2026 — the full Flutter hub.
- Flutter TextField and TextEditingController — the input this builds on.
- Flutter DropdownButton — another selection input.
- Flutter ListView.builder — render suggestion lists efficiently.
- Flutter HTTP Requests — back the search with an API.
Sources: Flutter documentation — the Autocomplete widget, SearchAnchor and SearchBar, and TextField (api.flutter.dev, docs.flutter.dev). Verified against current stable Flutter.