Segmented Control
iOS-style segmented control — sliding white selection on a muted track, icons & disabled
View component →A single, reusable Flutter bottom navigation bar driven by a list of items and a BottomNavStyle enum. It ships in three looks — a flat top-bordered bar, a bordered bar with vertical dividers between tabs, and a floating rounded pill that hovers above the content — with the active tab tinted lavender and optional labels under each icon. Built on the FlutterKit house theme, sized to a 64px touch-friendly bar. No external packages, fully DartPad-ready.
What's Included
Use Cases
PRO TIP
Keep a bottom bar to three to five destinations — past five, the tap targets shrink below comfort and a drawer or "more" tab reads better. Drive the selected tab from your own state via currentIndex and onTap so the bar never holds local truth, and hand it to the Scaffold bottomNavigationBar slot so it sits above the keyboard and respects safe areas. Reserve the floating-pill style for immersive screens where the bar should feel detached from the content.
Copy this into your Flutter project. No external packages required.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _index = 0;
static const _items = [
BottomNavItem(icon: Icons.home_outlined, label: 'Home'),
BottomNavItem(icon: Icons.account_balance_wallet_outlined, label: 'Wallet'),
BottomNavItem(icon: Icons.settings_outlined, label: 'Settings'),
BottomNavItem(icon: Icons.person_outline, label: 'Profile'),
];
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: const Color(0xFFFAFAFB),
body: const Center(
child: Text(
'Bottom navigation demo',
style: TextStyle(color: Color(0xFF8A8F98)),
),
),
bottomNavigationBar: AppBottomNavBar(
items: _items,
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
),
),
);
}
}
/// FlutterKit Bottom Navigation Bar — house design system (Linear lavender).
/// Flat, bordered, or floating-pill styles; the active tab tints lavender.
/// rohansurve.in/flutterkit/bottom-nav-bar
enum BottomNavStyle { flat, bordered, floating }
class BottomNavItem {
const BottomNavItem({required this.icon, required this.label});
/// Tab icon.
final IconData icon;
/// Tab label (hidden when showLabels is false).
final String label;
}
class AppBottomNavBar extends StatelessWidget {
const AppBottomNavBar({
super.key,
required this.items,
required this.currentIndex,
this.onTap,
this.style = BottomNavStyle.flat,
this.showLabels = true,
});
/// The destinations.
final List<BottomNavItem> items;
/// Index of the active tab.
final int currentIndex;
/// Called with the tapped index.
final ValueChanged<int>? onTap;
/// flat · bordered · floating.
final BottomNavStyle style;
/// Show the label under each icon.
final bool showLabels;
// ── House tokens — docs/flutterkit-design.md §1 ──
static const Color _accent = Color(0xFF5E6AD2);
static const Color _canvas = Color(0xFFFFFFFF);
static const Color _hairline = Color(0xFFE6E8EB);
static const Color _inkMuted = Color(0xFF51555E);
@override
Widget build(BuildContext context) {
final floating = style == BottomNavStyle.floating;
final bar = Container(
height: 64,
decoration: BoxDecoration(
color: _canvas,
borderRadius: floating ? BorderRadius.circular(999) : null,
border: floating
? Border.all(color: _hairline)
: const Border(top: BorderSide(color: _hairline)),
boxShadow: floating
? const [
BoxShadow(
color: Color(0x141C1E26),
blurRadius: 16,
offset: Offset(0, 4),
),
]
: null,
),
child: Material(
type: MaterialType.transparency,
child: Row(
children: List.generate(items.length, (i) {
final item = items[i];
final selected = i == currentIndex;
final color = selected ? _accent : _inkMuted;
final divided = style == BottomNavStyle.bordered && i != 0;
return Expanded(
child: InkWell(
onTap: onTap == null ? null : () => onTap!(i),
child: DecoratedBox(
decoration: divided
? const BoxDecoration(
border: Border(
left: BorderSide(color: _hairline),
),
)
: const BoxDecoration(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(item.icon, size: 24, color: color),
if (showLabels) ...[
const SizedBox(height: 4),
Text(
item.label,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
),
),
],
],
),
),
),
);
}),
),
),
);
if (floating) {
return Padding(
padding: const EdgeInsets.all(16),
child: ClipRRect(
borderRadius: BorderRadius.circular(999),
child: bar,
),
);
}
return bar;
}
}
// ── Usage ──
// AppBottomNavBar(items: items, currentIndex: i, onTap: (n) {}) // flat
// AppBottomNavBar(..., style: BottomNavStyle.bordered) // dividers
// AppBottomNavBar(..., style: BottomNavStyle.floating) // pill
// AppBottomNavBar(..., showLabels: false) // icons only
// Scaffold(bottomNavigationBar: AppBottomNavBar(...)) // in a ScaffoldiOS-style segmented control — sliding white selection on a muted track, icons & disabled
View component →One enum-driven button — 5 types, solid/pill/outline styles, 4 sizes, icon + loading
View component →One enum-driven badge — count, dot, label & status kinds on the house theme
View component →