📜 cyclic_tab_bar
A Flutter package for a cyclic tab bar with infinite scrolling, providing separated tab bar and tab bar view components similar to Flutter's official TabBar API.

✨ Features
- Separated Components: Use
CyclicTabBarandCyclicTabBarViewindependently for flexible layouts - Custom Decorations: Wrap the tab bar with any decoration (gradients, shadows, etc.)
- Infinite Scrolling: Seamlessly scroll through tabs and pages with wraparound support
- Controller-based: Coordinate multiple components with
CyclicTabController - Programmatic Control: Navigate to any tab index programmatically
- Customizable: Extensive styling options for tabs, indicators, and animations
✍️ Usage
Basic Example
import 'package:cyclic_tab_bar/cyclic_tab_bar.dart';
DefaultCyclicTabController(
contentLength: 10,
child: Column(
children: [
CyclicTabBar(
tabBuilder: (index, isSelected) => Text(
'Tab $index',
style: TextStyle(
color: isSelected ? Colors.pink : Colors.black54,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: CyclicTabBarView(
pageBuilder: (context, index, isSelected) => Center(
child: Text('Page $index'),
),
),
),
],
),
)
For very large tab counts, consider enabling forceFixedTabWidth to avoid expensive per-tab measurement.
Custom Tab Bar Decoration
One of the key benefits of the separated architecture is the ability to wrap the tab bar with custom decorations:
DefaultCyclicTabController(
contentLength: 10,
child: Column(
children: [
// Wrap tab bar with custom decoration
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade100, Colors.purple.shade100],
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: CyclicTabBar(
tabBuilder: (index, isSelected) => Text('Tab $index'),
indicatorColor: Colors.pink,
bottomBorder: BorderSide(color: Colors.black12, width: 2.0),
),
),
Expanded(
child: CyclicTabBarView(
pageBuilder: (context, index, isSelected) => Center(
child: Text('Page $index'),
),
onPageChanged: (index) => print('Page changed to $index'),
),
),
],
),
)
Using Explicit Controller
For more control, create your own CyclicTabController:
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
late CyclicTabController _controller;
@override
void initState() {
super.initState();
_controller = CyclicTabController(
contentLength: 10,
initialIndex: 5,
vsync: this,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
CyclicTabBar(
controller: _controller,
tabBuilder: (index, isSelected) => Text('Tab $index'),
),
Expanded(
child: CyclicTabBarView(
controller: _controller,
pageBuilder: (context, index, isSelected) => Center(
child: Text('Page $index'),
),
),
),
// Programmatic control
ElevatedButton(
onPressed: () => _controller.animateToIndex(7),
child: Text('Go to Tab 7'),
),
],
);
}
}
Controller utilities
CyclicTabController exposes ergonomic helpers for runtime reconfiguration:
setIndex(index, {bool animated = true})jumps or animates to a new tab without needing to tap.setContentLength(newLength, {int? selectedIndex, bool animated = false})updates the total tab/page count and optionally the current selection;CyclicTabBarandCyclicTabBarViewautomatically rebuild to reflect the change.enableHapticFeedbackcan be set tofalseto disable selection haptics.
These complement the lower-level animateToIndex and jumpToIndex methods shown above.
Customization Options
CyclicTabBar
tabBuilder: Builder function for tab widgets (can return any widget)controller: OptionalCyclicTabControlleronTabTap: Callback when a tab is tappedonTabLongPress: Callback when a tab is long pressedindicatorColor: Color of the selection indicatorindicatorHeight: Height of the indicatortabHeight: Height of the tab bartabPadding: Horizontal padding for each tabforceFixedTabWidth: Whether to use fixed width tabsfixedTabWidthFraction: Fraction of screen width for fixed tabsbottomBorder: Border separator between tabs and contentbackgroundColor: Background color of the tab bar
CyclicTabBarView
pageBuilder: Builder function for page widgetscontroller: OptionalCyclicTabControlleronPageChanged: Callback when the page changesscrollPhysics: Scroll physics for the page view
DefaultCyclicTabController
contentLength: Number of tabs/pagesinitialIndex: Initial selected index (default: 0)animationDuration: Duration of tab switching animationsenableHapticFeedback: Enable/disable selection haptics (default: true)child: The widget tree that will use the controller
🔧 Migration from Old API
If you were using the old combined CyclicTabBar widget (which included both tabs and pages), you can easily migrate:
Old API:
CyclicTabBar(
contentLength: 10,
tabBuilder: (index, isSelected) => Text('Tab $index'),
pageBuilder: (context, index, isSelected) => Text('Page $index'),
)
New API:
DefaultCyclicTabController(
contentLength: 10,
child: Column(
children: [
CyclicTabBar(
tabBuilder: (index, isSelected) => Text('Tab $index'),
),
Expanded(
child: CyclicTabBarView(
pageBuilder: (context, index, isSelected) => Text('Page $index'),
),
),
],
),
)
💭 Have a question?
If you have a question or found an issue, feel free to create an issue.