Tabs & Drawer
In React you build tab bars and navigation drawers with third-party libraries or custom components — there is no built-in tab navigation primitive. Flutter ships first-class widgets for every common navigation chrome pattern: BottomNavigationBar (or NavigationBar in Material 3) for bottom tabs, TabBar + TabBarView for top tabs, and Drawer for a side navigation menu. All three work with Scaffold, which reserves the correct layout space automatically.
Side-by-side comparison
Section titled “Side-by-side comparison”// React — bottom tabs (custom / library)import { useState } from 'react';
const tabs = ['Home', 'Search', 'Profile'];
function App() { const [activeTab, setActiveTab] = useState(0);
return ( <div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ flex: 1 }}> {activeTab === 0 && <HomeScreen />} {activeTab === 1 && <SearchScreen />} {activeTab === 2 && <ProfileScreen />} </div> <nav> {tabs.map((tab, i) => ( <button key={tab} onClick={() => setActiveTab(i)}> {tab} </button> ))} </nav> </div> );}// Flutter — BottomNavigationBarclass App extends StatefulWidget { const App({super.key}); @override State<App> createState() => _AppState();}
class _AppState extends State<App> { int _currentIndex = 0;
final _screens = const [ HomeScreen(), SearchScreen(), ProfileScreen(), ];
@override Widget build(BuildContext context) { return Scaffold( body: _screens[_currentIndex], bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) => setState(() => _currentIndex = index), items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), ], ), ); }}Runnable example
Section titled “Runnable example”import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: MainShell());
}
}
class MainShell extends StatefulWidget {
const MainShell({super.key});
@override
State<MainShell> createState() => _MainShellState();
}
class _MainShellState extends State<MainShell> {
int _index = 0;
static const _screens = [
_PlaceholderScreen(label: 'Home', icon: Icons.home, color: Colors.blue),
_PlaceholderScreen(label: 'Search', icon: Icons.search, color: Colors.green),
_PlaceholderScreen(label: 'Profile', icon: Icons.person, color: Colors.purple),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_screens[_index].label),
),
drawer: Drawer(
child: ListView(
children: const [
DrawerHeader(
decoration: BoxDecoration(color: Colors.blueGrey),
child: Text('Menu', style: TextStyle(color: Colors.white, fontSize: 24)),
),
ListTile(leading: Icon(Icons.settings), title: Text('Settings')),
ListTile(leading: Icon(Icons.help), title: Text('Help')),
],
),
),
body: IndexedStack(index: _index, children: _screens),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
class _PlaceholderScreen extends StatelessWidget {
final String label;
final IconData icon;
final Color color;
const _PlaceholderScreen({
required this.label,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: color),
const SizedBox(height: 12),
Text(label, style: TextStyle(fontSize: 24, color: color,
fontWeight: FontWeight.bold)),
],
),
);
}
}