image

11 Dec 2025

9K

35K

Creating a Custom Search Bar in Flutter

Flutter's rich set of customizable widgets empowers developers to build stunning and highly functional user interfaces. While Flutter provides default components for many common UI elements, there are often scenarios where a standard widget doesn't quite fit the specific design or functionality requirements. One such common need is a custom search bar.

A custom search bar offers several advantages over simply using a default TextField or a basic search icon in the AppBar. It allows for seamless integration with your app's theme, provides a more intuitive user experience, and can incorporate specific functionalities like animated transitions, search history, or advanced filtering options.

Understanding the Core Components

Building a custom search bar in Flutter typically involves combining several fundamental widgets and concepts:

  • TextField: This is the primary widget for user input. It will be the actual search input field.
  • TextEditingController: This controller is essential for managing and listening to changes in the TextField's text.
  • AppBar: Often, the search bar is integrated into the AppBar, either replacing the title or appearing as an overlay.
  • IconButton: Used for toggling the search bar's visibility (e.g., a search icon to open, a close/clear icon to close).
  • StatefulWidget: Since the search bar's state (whether it's open, what text is typed) changes, it needs to be managed within a StatefulWidget.
  • State Management: Variables to track the search state (e.g., _isSearching, _searchText) and to hold the list of items being searched and filtered.

Basic Implementation Steps

Let's walk through the fundamental steps to create a functional custom search bar that resides in the AppBar and filters a list of items.

Step 1: Set up the State

In your StatefulWidget, you'll need variables to manage the search bar's behavior:

  • A TextEditingController to control the input field.
  • A boolean flag (_isSearching) to determine if the search bar is active.
  • A string (_searchText) to store the current search query.
  • Two lists: one for all items (_allItems) and one for filtered results (_filteredItems).

Step 2: Design the UI (AppBar)

The AppBar will conditionally display either your app's title or the TextField, based on the _isSearching flag. It will also contain an IconButton to toggle the search state.

Step 3: Handle Input and Filtering

Attach a listener to your TextEditingController to react to text changes. Whenever the text changes, you'll update _searchText and re-filter your _allItems to populate _filteredItems.

Step 4: Display Results

The body of your Scaffold will then display the _filteredItems using a widget like ListView.builder.

Full Code Example

Here's a complete example demonstrating a custom search bar integrated into the AppBar, filtering a simple list of fruits:


import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Search Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.blueAccent, // Custom AppBar color
        ),
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  // Controller for the search TextField
  final TextEditingController _searchController = TextEditingController();
  // Flag to indicate if the search bar is currently active
  bool _isSearching = false;
  // Stores the current text in the search bar
  String _searchText = "";

  // The complete list of items to be searched
  final List _allItems = [
    'Apple', 'Banana', 'Cherry', 'Date', 'Elderberry',
    'Fig', 'Grape', 'Honeydew', 'Imbe', 'Jackfruit',
    'Kiwi', 'Lemon', 'Mango', 'Nectarine', 'Orange',
    'Papaya', 'Quince', 'Raspberry', 'Strawberry', 'Tangerine',
    'Ugli fruit', 'Vanilla', 'Watermelon', 'Xigua',
    'Yellow passionfruit', 'Zucchini' // Added a non-fruit for variety
  ];
  // The list of items displayed after filtering
  List _filteredItems = [];

  @override
  void initState() {
    super.initState();
    _filteredItems = _allItems; // Initially, display all items
    // Add a listener to the search controller to react to text changes
    _searchController.addListener(() {
      setState(() {
        _searchText = _searchController.text;
        _filterItems(); // Call filter logic on every text change
      });
    });
  }

  @override
  void dispose() {
    _searchController.dispose(); // Dispose the controller to prevent memory leaks
    super.dispose();
  }

  // Logic to filter items based on the current search text
  void _filterItems() {
    if (_searchText.isEmpty) {
      _filteredItems = _allItems; // If search text is empty, show all items
    } else {
      _filteredItems = _allItems
          .where((item) =>
              item.toLowerCase().contains(_searchText.toLowerCase()))
          .toList(); // Filter items that contain the search text (case-insensitive)
    }
  }

  // Toggles the search bar visibility and resets search state if closing
  void _toggleSearching() {
    setState(() {
      _isSearching = !_isSearching;
      if (!_isSearching) {
        _searchController.clear(); // Clear text when search is closed
        _searchText = "";
        _filterItems(); // Reset filtered items to all items
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Conditionally show TextField or app title
        title: _isSearching
            ? TextField(
                controller: _searchController,
                style: const TextStyle(color: Colors.white), // Text style for input
                cursorColor: Colors.white,
                decoration: const InputDecoration(
                  hintText: 'Search...',
                  hintStyle: TextStyle(color: Colors.white70),
                  border: InputBorder.none, // Remove default TextField border
                  contentPadding: EdgeInsets.symmetric(vertical: 0), // Adjust padding
                ),
                autofocus: true, // Automatically focus the TextField when it appears
              )
            : const Text('Custom Search Bar'),
        actions: [
          IconButton(
            icon: Icon(_isSearching ? Icons.close : Icons.search), // Change icon based on search state
            onPressed: _toggleSearching, // Toggle search on press
          ),
          // Optionally, add a clear button when search is active and text is present
          if (_isSearching && _searchText.isNotEmpty)
            IconButton(
              icon: const Icon(Icons.clear),
              onPressed: () {
                _searchController.clear();
                _searchText = "";
                _filterItems();
              },
            ),
        ],
      ),
      body: _filteredItems.isEmpty && _isSearching && _searchText.isNotEmpty
          ? const Center(child: Text("No results found.")) // Show message if no results and actively searching
          : ListView.builder(
              itemCount: _filteredItems.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_filteredItems[index]),
                );
              },
            ),
    );
  }
}

Enhancements and Best Practices

To make your custom search bar even more robust and user-friendly, consider these enhancements:

  • Debouncing Search Input:

    For large datasets or API calls, filtering on every keystroke can be inefficient. Implement debouncing using a Timer to wait for a short delay (e.g., 300-500ms) after the user stops typing before performing the search. This reduces unnecessary rebuilds or network requests.

    
    // Example for debouncing
    Timer? _debounce;
    void _onSearchTextChanged(String text) {
      if (_debounce?.isActive ?? false) _debounce!.cancel();
      _debounce = Timer(const Duration(milliseconds: 500), () {
        setState(() {
          _searchText = text;
          _filterItems();
        });
      });
    }
    // Then, in initState: _searchController.addListener(() => _onSearchTextChanged(_searchController.text));
    // And make sure to dispose _debounce.cancel() in dispose()
            
  • Animations:

    Use widgets like AnimatedSwitcher, AnimatedCrossFade, or AnimatedOpacity to create smooth transitions when the search bar appears or disappears, enhancing the visual appeal.

  • Focus Management:

    Control the keyboard's visibility and focus using a FocusNode. You might want to automatically show the keyboard when the search bar appears and hide it when the search is dismissed.

  • Keyboard Actions:

    Set the TextInputAction property of the TextField (e.g., TextInputAction.search) to provide a "Search" button on the keyboard. You can then listen to its submission event.

  • Clear Button:

    Always provide a clear button (like Icons.clear) within the TextField or as an IconButton in the AppBar to quickly empty the search field.

  • Handling Empty States:

    Display a friendly message when no search results are found or when the list is empty before any search.

  • Persistent Search:

    If appropriate, save recent search queries using shared_preferences or a local database to offer suggestions or a search history.

Conclusion

Creating a custom search bar in Flutter is a straightforward process that offers immense flexibility. By leveraging core widgets like TextField, AppBar, and managing state effectively, you can craft a search experience that perfectly aligns with your application's design language and functional requirements. Remember to consider user experience enhancements like debouncing and animations to deliver a truly polished feature.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is