curved_dual_tab_bar
A curved, two-segment tab bar with an animated S-curve divider — built on a real Material TabBar, not a fake gesture layer.
▶ Live Demo — every styling knob demoed interactively.
- 🎯 A real
TabBarunderneath — tap targets, the selection indicator, ripple/hover/focus feedback and a11y semantics all come from Flutter's ownTabBar; this widget only paints the curved background around it. - 🎨 Fully themeable — solid colors, gradients, per-side borders, divider color/width, corner radius, lean amplitude, animation duration/curve.
- 🧩 Two ways to use it —
CurvedDualTabBar(drop-in header, owns its ownTabController) orCurvedTabBackground(the standalone curve painter, if you already drive a nativeTabBaryourself).
Screenshot
| Demo app |
|---|
![]() |
| Captured on a physical Android device from the example app |
Try every variant live at the demo site.
Install
dependencies:
curved_dual_tab_bar: ^0.2.0
Usage
import 'package:curved_dual_tab_bar/curved_dual_tab_bar.dart';
CurvedDualTabBar(
titles: const ['DEPOSIT', 'WITHDRAW'],
selectedIndex: selectedIndex,
onChanged: (i) => setState(() => selectedIndex = i),
)
That's a fully working, animated, two-tab header — CurvedDualTabBar creates and owns its own TabController internally, kept in sync with selectedIndex/onChanged.
Already have a native TabBar?
Use CurvedTabBackground as pure decoration and let your own TabController drive it:
CurvedTabBackground(
selectedIndex: selectedIndex,
progress: tabController.animation,
child: TabBar(
controller: tabController,
indicatorColor: Colors.transparent,
dividerColor: Colors.transparent,
tabs: const [Tab(text: 'LEFT'), Tab(text: 'RIGHT')],
),
)
Styling
Both widgets accept activeColor/inactiveColor (or activeGradient/inactiveGradient), unselectedColor/unselectedBorderColor, activeBorderColor/inactiveBorderColor, borderRadius, leanAmplitude (how much the S-curve leans), and duration/curve for the transition animation. See the example app for every combination demoed side by side.
Divider styling
The seam itself has its own set of knobs, independent of the fill colors:
CurvedDualTabBar(
titles: const ['DEPOSIT', 'WITHDRAW'],
selectedIndex: selectedIndex,
onChanged: (i) => setState(() => selectedIndex = i),
dividerGradient: const LinearGradient(colors: [Colors.pink, Colors.purple, Colors.blue]),
dividerWidth: 3,
dividerCap: StrokeCap.round,
dividerShadow: const BoxShadow(color: Colors.black26, blurRadius: 10, spreadRadius: 1),
topControlOffset: const Offset(16, 6),
bottomControlOffset: const Offset(-16, -6),
)
dividerColor/dividerWidth— solid stroke color and width.dividerGradient— overridesdividerColorwith a gradient stroke.dividerCap—StrokeCap.butt(default, flat cut at the top/bottom edges) or.round/.square.dividerShadow— aBoxShadowstroked once behind the seam as a soft glow/shadow; itsspreadRadiuswidens the stroke rather than growing a filled shape.topControlOffset/bottomControlOffset— nudge the curve's own bezier control points directly. The curve's default shape keeps a flat tangent at the top/bottom edges (so it grazes them like a wave crest/trough); these offsets break that and let you reshape the S-curve independent ofleanAmplitude.
Why exactly 2 tabs?
The curve divider is a single S-shaped seam between an active and an inactive half — that only has a well-defined shape for 2 segments. CurvedDualTabBar asserts titles.length == 2 for that reason. For more segments, use a plain TabBar.
License
MIT — see LICENSE.
Libraries
- curved_dual_tab_bar
- Curved dual-segment tab bar with an animated S-curve divider.
