Implementing Custom Tab Bars in Flutter

Tab bars are an essential component of many mobile applications, providing a clear and intuitive way to navigate between different sections. Flutter offers a default TabBar widget, but sometimes you need a custom design that aligns with your app’s unique branding and user experience goals. This blog post will guide you through the process of implementing custom tab bars in Flutter.

Why Use Custom Tab Bars?

  • Branding: Align the tab bar’s appearance with your app’s unique branding.
  • Improved User Experience: Enhance usability with custom animations and interactions.
  • Unique Features: Incorporate features beyond basic tab switching, such as animated indicators or custom icons.

Implementing Custom Tab Bars in Flutter

To create a custom tab bar, you’ll typically use a combination of Flutter’s built-in widgets, such as Row, GestureDetector, and AnimatedContainer.

Step 1: Set Up the Basic Structure

Start by creating a basic structure using a Row to hold the tab items and a GestureDetector to handle tap events.


import 'package:flutter/material.dart';

class CustomTabBar extends StatefulWidget {
  final List tabs;
  final Function(int) onTabSelected;

  const CustomTabBar({Key? key, required this.tabs, required this.onTabSelected}) : super(key: key);

  @override
  _CustomTabBarState createState() => _CustomTabBarState();
}

class _CustomTabBarState extends State {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: List.generate(widget.tabs.length, (index) {
        return GestureDetector(
          onTap: () {
            setState(() {
              _selectedIndex = index;
              widget.onTabSelected(index);
            });
          },
          child: Text(widget.tabs[index]),
        );
      }),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom Tab Bar Example')),
      body: Column(
        children: [
          CustomTabBar(
            tabs: const ['Home', 'Search', 'Profile'],
            onTabSelected: (index) {
              print('Selected Tab Index: $index');
            },
          ),
          const Expanded(
            child: Center(
              child: Text('Content for Selected Tab'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(home: HomeScreen()));
}

Step 2: Style the Tab Items

Add styling to make the tab items visually appealing and interactive.


import 'package:flutter/material.dart';

class CustomTabBar extends StatefulWidget {
  final List tabs;
  final Function(int) onTabSelected;

  const CustomTabBar({Key? key, required this.tabs, required this.onTabSelected}) : super(key: key);

  @override
  _CustomTabBarState createState() => _CustomTabBarState();
}

class _CustomTabBarState extends State {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: List.generate(widget.tabs.length, (index) {
          return GestureDetector(
            onTap: () {
              setState(() {
                _selectedIndex = index;
                widget.onTabSelected(index);
              });
            },
            child: Container(
              padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
              decoration: BoxDecoration(
                color: _selectedIndex == index ? Colors.blue : Colors.transparent,
                borderRadius: BorderRadius.circular(8.0),
              ),
              child: Text(
                widget.tabs[index],
                style: TextStyle(
                  color: _selectedIndex == index ? Colors.white : Colors.black,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          );
        }),
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom Tab Bar Example')),
      body: Column(
        children: [
          CustomTabBar(
            tabs: const ['Home', 'Search', 'Profile'],
            onTabSelected: (index) {
              print('Selected Tab Index: $index');
            },
          ),
          const Expanded(
            child: Center(
              child: Text('Content for Selected Tab'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(home: HomeScreen()));
}

Step 3: Add an Animated Indicator

Enhance the visual feedback by adding an animated indicator to highlight the selected tab.


import 'package:flutter/material.dart';

class CustomTabBar extends StatefulWidget {
  final List tabs;
  final Function(int) onTabSelected;

  const CustomTabBar({Key? key, required this.tabs, required this.onTabSelected}) : super(key: key);

  @override
  _CustomTabBarState createState() => _CustomTabBarState();
}

class _CustomTabBarState extends State {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Stack(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: List.generate(widget.tabs.length, (index) {
              return GestureDetector(
                onTap: () {
                  setState(() {
                    _selectedIndex = index;
                    widget.onTabSelected(index);
                  });
                },
                child: Container(
                  padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
                  child: Text(
                    widget.tabs[index],
                    style: TextStyle(
                      color: _selectedIndex == index ? Colors.white : Colors.black,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              );
            }),
          ),
          AnimatedPositioned(
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeInOut,
            left: _selectedIndex * (MediaQuery.of(context).size.width / widget.tabs.length),
            top: 0,
            bottom: 0,
            child: Container(
              width: MediaQuery.of(context).size.width / widget.tabs.length,
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(8.0),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom Tab Bar Example')),
      body: Column(
        children: [
          CustomTabBar(
            tabs: const ['Home', 'Search', 'Profile'],
            onTabSelected: (index) {
              print('Selected Tab Index: $index');
            },
          ),
          const Expanded(
            child: Center(
              child: Text('Content for Selected Tab'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(home: HomeScreen()));
}

Step 4: Customize with Icons

Incorporate custom icons to make the tab bar more visually informative.


import 'package:flutter/material.dart';

class CustomTabBar extends StatefulWidget {
  final List tabs;
  final List icons; // List of icons
  final Function(int) onTabSelected;

  const CustomTabBar({Key? key, required this.tabs, required this.icons, required this.onTabSelected}) : super(key: key);

  @override
  _CustomTabBarState createState() => _CustomTabBarState();
}

class _CustomTabBarState extends State {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Stack(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: List.generate(widget.tabs.length, (index) {
              return GestureDetector(
                onTap: () {
                  setState(() {
                    _selectedIndex = index;
                    widget.onTabSelected(index);
                  });
                },
                child: Column(
                  children: [
                    Icon(
                      widget.icons[index], // Display corresponding icon
                      color: _selectedIndex == index ? Colors.white : Colors.black,
                    ),
                    Text(
                      widget.tabs[index],
                      style: TextStyle(
                        color: _selectedIndex == index ? Colors.white : Colors.black,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
              );
            }),
          ),
          AnimatedPositioned(
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeInOut,
            left: _selectedIndex * (MediaQuery.of(context).size.width / widget.tabs.length),
            top: 0,
            bottom: 0,
            child: Container(
              width: MediaQuery.of(context).size.width / widget.tabs.length,
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(8.0),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom Tab Bar Example')),
      body: Column(
        children: [
          CustomTabBar(
            tabs: const ['Home', 'Search', 'Profile'],
            icons: const [Icons.home, Icons.search, Icons.person], // List of icons
            onTabSelected: (index) {
              print('Selected Tab Index: $index');
            },
          ),
          const Expanded(
            child: Center(
              child: Text('Content for Selected Tab'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(home: HomeScreen()));
}

Conclusion

Implementing custom tab bars in Flutter provides a way to create visually appealing and user-friendly navigation. By combining Flutter’s widgets and animation capabilities, you can craft a unique tab bar that perfectly complements your app’s branding and enhances the user experience.