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.

pub.dev license Demo

▶ Live Demo — every styling knob demoed interactively.

  • 🎯 A real TabBar underneath — tap targets, the selection indicator, ripple/hover/focus feedback and a11y semantics all come from Flutter's own TabBar; 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 own TabController) or CurvedTabBackground (the standalone curve painter, if you already drive a native TabBar yourself).

Screenshot

Demo app
CurvedDualTabBar demo
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 — overrides dividerColor with a gradient stroke.
  • dividerCap — StrokeCap.butt (default, flat cut at the top/bottom edges) or .round/.square.
  • dividerShadow — a BoxShadow stroked once behind the seam as a soft glow/shadow; its spreadRadius widens 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 of leanAmplitude.

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.