bottom_navigation_animated_notch_bar 1.1.0
bottom_navigation_animated_notch_bar: ^1.1.0 copied to clipboard
A uniquely designed, highly customizable animated notch bottom navigation bar for Flutter with smooth transitions and glassmorphism support.
bottom_navigation_animated_notch_bar #
A uniquely designed, highly customizable animated notch bottom navigation bar for Flutter.
Live Action: Smooth, Dynamic Navigation #
A fluid, modern navigation experience to keep your users engaged.

Try the Interactive Demo: #
- Try Android | iOS | Web Demo โ See the live animated notch bar across all platforms.
๐ค FAQ: Why install this instead of just copy-pasting the code? #
1. Clean Codebase: If you copy the source code directly, you'll have to add 300+ lines of complex animation logic, math, and custom painters to your app. Installing the package hides all this complexity away.
2. Easy to Use: Once installed, you can add this beautiful navigation bar to your app with just a few lines of clean, readable code:
// 1. Import the package
import 'package:bottom_navigation_animated_notch_bar/bottom_navigation_animated_notch_bar.dart';
// 2. Add the widget to your Scaffold
AnimatedNotchBottomBar(
notchBottomBarController: NotchBottomBarController(index: 0),
color: Colors.white,
notchColor: Colors.black87,
bottomBarItems: const [
BottomBarItem(
inActiveItem: Icon(Icons.home_outlined, color: Colors.grey),
activeItem: Icon(Icons.home_filled, color: Colors.white),
itemLabel: 'Home',
),
BottomBarItem(
inActiveItem: Icon(Icons.search_outlined, color: Colors.grey),
activeItem: Icon(Icons.search, color: Colors.white),
itemLabel: 'Search',
),
],
onTap: (index) {
// Handle tab change
},
)
3. Future Updates:
When we add new features (like spring animations or haptics), you get them instantly by updating your pubspec.yaml instead of manually copying new code and resolving merge conflicts!
๐ Package Installation #
1. Add Dependency #
Add the package to your pubspec.yaml file:
dependencies:
bottom_navigation_animated_notch_bar: ^1.0.3
Or use the terminal command:
flutter pub add bottom_navigation_animated_notch_bar
2. Import the Package #
import 'package:bottom_navigation_animated_notch_bar/bottom_navigation_animated_notch_bar.dart';
About This Example #
This example app (example/) is a complete, production-quality demo of how to integrate bottom_navigation_animated_notch_bar into any Flutter application. It demonstrates:
- Integrating the package with
GetXstate management - Responsive layout that adapts between mobile (notch bar) and desktop (top nav bar)
- Scroll-to-section navigation using
GlobalKey - Animated section reveals with
flutter_animateandvisibility_detector - The
AnimatedNotchBottomBarwidget with all key parameters configured
Key Package Integration #
The core of this example is in lib/main.dart. Here is the essential widget usage:
// 1. Import the package
import 'package:bottom_navigation_animated_notch_bar/bottom_navigation_animated_notch_bar.dart';
// 2. Create a controller (manages active index)
final controller = NotchBottomBarController(index: 0);
// 3. Add the widget
AnimatedNotchBottomBar(
notchBottomBarController: controller,
color: const Color(0xFF2A2D3E), // Bar background color
notchColor: const Color(0xFF151521), // Circle/notch dark background
activeIconColor: const Color(0xFF00E5FF), // Glowing active icon color
showLabel: true,
removeMargins: true,
bottomBarHeight: 72.0,
bottomBarItems: const [
BottomBarItem(
inActiveItem: Icon(Icons.home_outlined, color: Colors.white60),
activeItem: Icon(Icons.home_filled),
itemLabel: 'Home',
),
BottomBarItem(
inActiveItem: Icon(Icons.search_outlined, color: Colors.white60),
activeItem: Icon(Icons.search),
itemLabel: 'Search',
),
// ... up to 5 items
],
onTap: (index) {
controller.jumpTo(index);
// Navigate to the selected section
},
),
The entire example lives in a single file: lib/main.dart
Prerequisites #
Before running this example, ensure you have the following installed:
| Tool | Minimum Version | Check Command |
|---|---|---|
| Flutter SDK | 3.16.0 | flutter --version |
| Dart SDK | 3.2.0 | dart --version |
| Android Studio / Xcode | Latest | โ |
| Chrome (for Web) | Latest | โ |
Step-by-Step Setup #
Step 1 โ Clone the Repository #
git clone https://github.com/Crealify/bottom_navigation_animated_notch_bar.git
cd bottom_navigation_animated_notch_bar
Step 2 โ Get Dependencies for the Example #
cd example
flutter pub get
Note
The example depends on the local parent package via path: ../. No separate package installation is needed.
Step 3 โ Choose Your Target Platform #
๐ค Running on Android #
Requirements #
- Android Studio with Android SDK installed
- A physical Android device (API 21+) or an Android Emulator (API 21+)
Steps #
1. Connect your device or start an emulator:
# List available devices
flutter devices
# Example output:
# sdk gphone64 x86 64 โข emulator-5554 โข android-x64 โข Android 14 (API 34)
# My Phone โข ABC123DEF456 โข android-arm โข Android 13 (API 33)
2. Run the example on Android:
cd example
flutter run -d android
Or target a specific device ID:
flutter run -d emulator-5554
3. What to expect:
- On a phone screen (width < 850dp), the Animated Notch Bottom Bar appears at the bottom
- Tap any tab to see the smooth notch animation slide to the active position
- Scroll down to trigger section-reveal animations
- The bar auto-hides slightly when scrolling down and reappears on scroll up
Tip
For best performance on Android, run with --release flag: flutter run --release -d android
๐ Running on iOS #
Requirements #
- macOS with Xcode 14+ installed
- An iOS Simulator or a physical iPhone/iPad (iOS 12+)
- Apple Developer account (for physical device deployment)
Steps #
1. Install iOS pods (first time only):
cd example/ios
pod install
cd ..
2. List available iOS simulators:
flutter devices
# iPhone 15 Pro โข 12345678-ABCD-... โข ios โข com.apple.CoreSimulator...
3. Run on iOS Simulator:
cd example
flutter run -d iphone
Or open in Xcode for more control:
open ios/Runner.xcworkspace
Then press โถ Run in Xcode.
4. Run on a physical iPhone:
# Trust your Mac on the device first, then:
flutter run -d <your-device-id>
5. What to expect on iOS:
- The notch bar respects iOS Safe Area (bottom home indicator)
- Smooth
easeOutCubicanimation matches iOS's native feel - The glassmorphic top navigation bar uses
BackdropFilterfor the blur effect
Important
If you see a signing error, open ios/Runner.xcworkspace in Xcode, go to Signing & Capabilities, and select your Apple Developer Team.
๐ Running on Web #
Requirements #
- Google Chrome (recommended) or any modern browser
- No additional setup needed โ Flutter Web works out of the box
Steps #
1. Enable Flutter Web (if not already enabled):
flutter config --enable-web
2. Run in Chrome:
cd example
flutter run -d chrome
3. Run on a custom port:
flutter run -d web-server --web-port 8080
# Then open http://localhost:8080 in any browser
4. Build a production web bundle:
flutter build web --release
# Output in: example/build/web/
To serve it locally:
cd build/web
python -m http.server 8080
5. What to expect on Web:
- On wide screens (โฅ 1100px), the top glassmorphic navigation bar is shown
- On narrow screens (< 850px), the Animated Notch Bottom Bar appears at the bottom
- Resize the browser window to see the responsive switch between layouts
- Mouse hover effects activate on all interactive elements
Tip
Use Chrome DevTools โ Toggle Device Toolbar (Ctrl+Shift+M) to simulate mobile layouts.
๐ป Full Interactive Example Source Code #
The entire example lives in a single file: lib/main.dart
\n\n
๐ป Full Interactive Example Source Code #
The entire example lives in a single file: lib/main.dart
โจ Click to Expand Complete main.dart Implementation
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:visibility_detector/visibility_detector.dart';
// Import our new professional package
import 'package:bottom_navigation_animated_notch_bar/bottom_navigation_animated_notch_bar.dart';
// =============================================================================
// 1. MAIN ENTRY POINT
// =============================================================================
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Notch Bar Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: AppConstants.bgColor,
primaryColor: AppConstants.primaryColor,
colorScheme: const ColorScheme.dark(primary: AppConstants.primaryColor),
),
home: const MainViewWrapper(),
);
}
}
// =============================================================================
// 2. CONSTANTS & CONTROLLERS
// =============================================================================
class AppConstants {
static const Color bgColor = Color(0xFF1A1A29);
static const Color cardColor = Color(0xFF2A2D3E);
static const Color darkColor = Color(0xFF151521);
static const Color primaryColor = Color(0xFF00E5FF);
static const Color secondaryColor = Color(0xFFFF4D8D);
static const Color activeIconColor = Color(0xFF00E5FF);
static const double spacing24 = 24.0;
static const double spacing64 = 64.0;
}
class AppController extends GetxController {
final scrollController = ScrollController();
// A ValueNotifier that holds the current scroll offset.
// Only the ListView attaches to scrollController; everything else reads this notifier.
final scrollOffset = ValueNotifier<double>(0.0);
// Using the controller from the package
final notchBottomBarController = NotchBottomBarController(index: 0);
final isMenuOpen = false.obs;
final isNavbarVisible = true.obs;
final isHeaderVisible = true.obs;
static final section1Key = GlobalKey();
static final section2Key = GlobalKey();
static final section3Key = GlobalKey();
static final section4Key = GlobalKey();
static final section5Key = GlobalKey();
@override
void onInit() {
super.onInit();
scrollController.addListener(_onScroll);
}
void _onScroll() {
if (scrollController.hasClients && scrollController.positions.length == 1) {
scrollOffset.value = scrollController.offset;
}
}
@override
void onClose() {
scrollController.removeListener(_onScroll);
scrollController.dispose();
scrollOffset.dispose();
super.onClose();
}
void scrollToTop() {
if (scrollController.hasClients) {
scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
void scrollToSection(GlobalKey key) {
if (key.currentContext != null) {
Scrollable.ensureVisible(key.currentContext!, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
}
final appController = Get.put(AppController());
final globalScrollController = appController.scrollController;
class PortfolioController extends GetxController {
void trackAction(String action) {
debugPrint("Tracked action: $action");
}
}
// =============================================================================
// 3. RESPONSIVE UTILS
// =============================================================================
class Responsive extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const Responsive({super.key, required this.mobile, this.tablet, required this.desktop});
static bool isMobile(BuildContext context) => MediaQuery.sizeOf(context).width < 850;
static bool isTablet(BuildContext context) => MediaQuery.sizeOf(context).width >= 850 && MediaQuery.sizeOf(context).width < 1100;
static bool isDesktop(BuildContext context) => MediaQuery.sizeOf(context).width >= 1100;
static bool isExtraLargeScreen(BuildContext context) => MediaQuery.sizeOf(context).width >= 1400;
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.sizeOf(context);
if (size.width >= 1100) return desktop;
if (size.width >= 850 && tablet != null) return tablet!;
return mobile;
}
}
// =============================================================================
// 4. MOCK SECTIONS
// =============================================================================
class _PlaceholderPage extends StatelessWidget {
final String title;
final Color color;
const _PlaceholderPage(this.title, this.color);
@override
Widget build(BuildContext context) {
return Container(
height: 600,
width: double.infinity,
decoration: BoxDecoration(color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withValues(alpha: 0.5))),
child: Center(child: Text(title, style: TextStyle(fontSize: 32, color: color, fontWeight: FontWeight.bold))),
);
}
}
class MainViewWrapper extends StatelessWidget {
const MainViewWrapper({super.key});
@override
Widget build(BuildContext context) {
return const MainView(pages: [
_PlaceholderPage("Home View", Colors.blueAccent),
_PlaceholderPage("Search View", Colors.greenAccent),
_PlaceholderPage("Favorites View", Colors.orangeAccent),
_PlaceholderPage("Settings View", Colors.purpleAccent),
_PlaceholderPage("Profile View", Colors.redAccent),
]);
}
}
class CustomDrawer extends StatelessWidget { const CustomDrawer({super.key}); @override Widget build(BuildContext context) => const Drawer(child: Center(child: Text("Drawer Content", style: TextStyle(color: Colors.white)))); }
class MeshBackground extends StatelessWidget { final ScrollController scrollController; const MeshBackground({super.key, required this.scrollController}); @override Widget build(BuildContext context) => const SizedBox.shrink(); }
class PluginsSection extends StatelessWidget { const PluginsSection({super.key}); @override Widget build(BuildContext context) => const SizedBox.shrink(); }
class FeaturedVideos extends StatelessWidget { const FeaturedVideos({super.key}); @override Widget build(BuildContext context) => const SizedBox.shrink(); }
class ShortsVibes extends StatelessWidget { const ShortsVibes({super.key}); @override Widget build(BuildContext context) => const SizedBox.shrink(); }
class CollegeMemories extends StatelessWidget { const CollegeMemories({super.key}); @override Widget build(BuildContext context) => const SizedBox.shrink(); }
class PortfolioEnding extends StatelessWidget { const PortfolioEnding({super.key}); @override Widget build(BuildContext context) => const _PlaceholderPage("End of Content", Colors.deepPurple); }
class NavigationButtonList extends StatelessWidget {
const NavigationButtonList({super.key});
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(onPressed: () => appController.scrollToSection(AppController.section1Key), child: const Text("Home", style: TextStyle(color: Colors.white))),
const SizedBox(width: 4),
TextButton(onPressed: () => appController.scrollToSection(AppController.section2Key), child: const Text("Search", style: TextStyle(color: Colors.white))),
const SizedBox(width: 4),
TextButton(onPressed: () => appController.scrollToSection(AppController.section3Key), child: const Text("Favorites", style: TextStyle(color: Colors.white))),
const SizedBox(width: 4),
TextButton(onPressed: () => appController.scrollToSection(AppController.section4Key), child: const Text("Settings", style: TextStyle(color: Colors.white))),
const SizedBox(width: 4),
TextButton(onPressed: () => appController.scrollToSection(AppController.section5Key), child: const Text("Profile", style: TextStyle(color: Colors.white))),
],
),
);
}
}
// =============================================================================
// 5. TOP NAVIGATION BAR (For Desktop)
// =============================================================================
class TopNavigationBar extends StatefulWidget {
const TopNavigationBar({super.key});
@override
State<TopNavigationBar> createState() => _TopNavigationBarState();
}
class _TopNavigationBarState extends State<TopNavigationBar> {
bool _isHovered = false;
bool _isLogoHovered = false;
@override
Widget build(BuildContext context) {
final double width = MediaQuery.sizeOf(context).width;
final bool showFullNav = width >= 1100;
return Obx(() {
final bool isMenuOpen = appController.isMenuOpen.value;
return ValueListenableBuilder<double>(
valueListenable: appController.scrollOffset,
builder: (context, offset, child) {
final bool isScrolled = offset > 50;
final double glassOpacity = isScrolled ? 0.65 : 0.2;
final double blurAmount = isScrolled ? 24.0 : 12.0;
final double horizontalPadding = showFullNav ? AppConstants.spacing24 : 16.0;
final double topMargin = isScrolled ? 0.0 : 0.0;
return Align(
alignment: Alignment.topCenter,
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
margin: EdgeInsets.only(
top: topMargin,
left: isMenuOpen ? 12.0 : AppConstants.spacing24,
right: isMenuOpen ? 12.0 : AppConstants.spacing24),
height: 72,
constraints: BoxConstraints(maxWidth: isMenuOpen ? 1100 : 1200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.shadowColor
.withValues(alpha: isScrolled ? 0.15 : 0.05),
blurRadius: isScrolled ? 30 : 15,
offset: Offset(0, isScrolled ? 15 : 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(40),
child: BackdropFilter(
filter:
ImageFilter.blur(sigmaX: blurAmount, sigmaY: blurAmount),
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
border: Border.all(
color: Colors.white.withValues(alpha: 0.15),
width: 1.2,
),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context)
.scaffoldBackgroundColor
.withValues(alpha: glassOpacity),
Theme.of(context)
.scaffoldBackgroundColor
.withValues(alpha: glassOpacity * 0.8),
],
),
),
padding:
EdgeInsets.symmetric(horizontal: horizontalPadding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildLogo(),
if (showFullNav)
const Expanded(
child: Center(child: NavigationButtonList()),
),
if (!showFullNav) const Spacer(),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildAdminButton(),
const SizedBox(width: 8),
_buildHireMeButton(context),
],
),
),
),
],
),
),
),
),
),
);
},
);
});
}
Widget _buildLogo() {
return MouseRegion(
onEnter: (_) => setState(() => _isLogoHovered = true),
onExit: (_) => setState(() => _isLogoHovered = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => appController.scrollToTop(),
child: AnimatedRotation(
turns: _isLogoHovered ? 0.05 : 0,
duration: const Duration(milliseconds: 300),
child: const SizedBox(
height: 45,
child: Icon(Icons.code, size: 40, color: Colors.white),
),
),
),
);
}
Widget _buildHireMeButton(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() {
_isHovered = false;
}),
cursor: SystemMouseCursors.click,
child: PopupMenuButton<String>(
tooltip: 'Contact',
offset: const Offset(0, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
color: AppConstants.cardColor,
onSelected: (value) async {
const email = 'contact@example.com';
const subject = 'Inquiry';
if (value == 'gmail') {
final gmailUrl = Uri(
scheme: 'https',
host: 'mail.google.com',
path: '/mail/',
queryParameters: {
'view': 'cm',
'fs': '1',
'to': email,
'su': subject
});
final mailtoUrl = Uri(
scheme: 'mailto',
path: email,
queryParameters: {'subject': subject});
if (!await launchUrl(mailtoUrl)) {
await launchUrl(gmailUrl, mode: LaunchMode.externalApplication);
}
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'gmail',
child: Row(children: [
Icon(Icons.email, color: Colors.redAccent),
SizedBox(width: 10),
Text('Gmail', style: TextStyle(color: Colors.white))
]),
),
],
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOutCubic,
transform: Matrix4.diagonal3Values(
_isHovered ? 1.05 : 1.0,
_isHovered ? 1.05 : 1.0,
1.0,
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
gradient: const LinearGradient(
colors: [Color(0xFFFF4D8D), Color(0xFF6C7BFF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF6C7BFF)
.withValues(alpha: _isHovered ? 0.6 : 0.4),
blurRadius: _isHovered ? 20 : 12,
offset: const Offset(0, 5),
)
],
),
child: const Text(
"Contact",
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14),
),
),
),
);
}
Offset _adminMagneticOffset = Offset.zero;
Widget _buildAdminButton() {
return MouseRegion(
onExit: (_) => setState(() {
_adminMagneticOffset = Offset.zero;
}),
onHover: (event) {
final RenderBox? box = context.findRenderObject() as RenderBox?;
if (box != null) {
final Offset center = box.size.center(Offset.zero);
final Offset localPos = event.localPosition;
setState(() {
_adminMagneticOffset = Offset(
(localPos.dx - center.dx) * 0.1,
(localPos.dy - center.dy) * 0.1,
);
});
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOutCubic,
transform: Matrix4.translationValues(
_adminMagneticOffset.dx, _adminMagneticOffset.dy, 0),
child: IconButton(
icon: Icon(Icons.admin_panel_settings,
color: Theme.of(context).colorScheme.primary),
onPressed: () {},
tooltip: 'Admin Dashboard',
),
),
);
}
}
// =============================================================================
// 6. MAIN VIEW (Integrating the Package)
// =============================================================================
class MainView extends StatelessWidget {
const MainView({super.key, required this.pages});
final List<Widget> pages;
@override
Widget build(BuildContext context) {
Get.put(PortfolioController());
final bool isDesktop = Responsive.isDesktop(context);
final bool isLarge = Responsive.isExtraLargeScreen(context);
return Scaffold(
backgroundColor: AppConstants.bgColor,
drawer: isDesktop ? null : const CustomDrawer(),
body: Row(
children: [
if (isDesktop)
Obx(() => AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOutCubic,
width: appController.isMenuOpen.value
? (isLarge ? 320 : 280)
: 0,
decoration: BoxDecoration(
color: Colors.transparent,
boxShadow: [
if (appController.isMenuOpen.value)
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 40,
spreadRadius: 10,
),
],
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
child: SizedBox(
width: isLarge ? 320 : 280,
child: const CustomDrawer(),
),
),
)),
Expanded(
child: Obx(() => AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOutCubic,
margin: appController.isMenuOpen.value
? const EdgeInsets.only(
top: 0, left: 12, right: 12, bottom: 12)
: EdgeInsets.zero,
decoration: BoxDecoration(
color: AppConstants.bgColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
topRight: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
bottomLeft: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
bottomRight: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
),
boxShadow: [
if (appController.isMenuOpen.value)
BoxShadow(
color: Colors.white.withValues(alpha: 0.04),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
topRight: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
bottomLeft: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
bottomRight: Radius.circular(
appController.isMenuOpen.value ? 24 : 0),
),
child: Stack(
children: [
if (isDesktop)
RepaintBoundary(
child: MeshBackground(
scrollController: globalScrollController),
),
if (isDesktop) const _MouseHalo(),
Builder(
builder: (context) {
final List<Widget> sections = [
_buildSection(AppController.section1Key, pages[0]),
_SectionDivider(isDesktop: isDesktop),
_buildSection(AppController.section2Key, pages[1]),
_SectionDivider(isDesktop: isDesktop),
_buildSection(AppController.section3Key, pages[2]),
_SectionDivider(isDesktop: isDesktop),
_buildSection(AppController.section4Key, pages[3]),
_SectionDivider(isDesktop: isDesktop),
_buildSection(AppController.section5Key, pages[4]),
_SectionDivider(isDesktop: isDesktop),
const PortfolioEnding(),
if (!isDesktop)
Obx(() => SizedBox(
height: appController.isNavbarVisible.value ? 100 : 20,
)),
];
return ListView.builder(
controller: globalScrollController,
physics: const BouncingScrollPhysics(),
cacheExtent: 2000,
addAutomaticKeepAlives: true,
addRepaintBoundaries: true,
padding: EdgeInsets.zero,
itemCount: sections.length,
itemBuilder: (context, index) {
final sectionWidget = sections[index];
final isFirst = index == 0;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1200),
child: Padding(
padding: EdgeInsets.only(
top: isFirst ? (isDesktop ? 80 : 40) : 0,
),
child: (sectionWidget is _SectionDivider || sectionWidget is SizedBox)
? sectionWidget
: Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? AppConstants.spacing64 : AppConstants.spacing24,
),
child: _DynamicSection(
index: index,
animKey: 'section_anim_$index',
child: sectionWidget,
),
),
),
),
);
},
);
},
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Obx(() => AnimatedSlide(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOutCubic,
offset: appController.isHeaderVisible.value
? Offset.zero
: const Offset(0, -1.5),
child: SafeArea(
bottom: false,
child: Padding(
padding:
EdgeInsets.only(top: isDesktop ? 0 : 2),
child: const RepaintBoundary(
child: TopNavigationBar()),
),
),
)),
),
if (!isDesktop)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Obx(() {
return AnimatedSlide(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOutCubic,
offset: appController.isNavbarVisible.value
? Offset.zero
: const Offset(0, 1.5),
child: MediaQuery.removePadding(
context: context,
removeBottom: true,
child: Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.85),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
// THIS IS THE PACKAGE WIDGET:
child: AnimatedNotchBottomBar(
notchBottomBarController:
appController.notchBottomBarController,
color: AppConstants.cardColor,
showLabel: true,
notchColor: AppConstants.darkColor,
removeMargins: true,
bottomBarHeight: 72.0,
activeIconColor: AppConstants.activeIconColor,
bottomBarItems: const [
BottomBarItem(
inActiveItem: Icon(Icons.home_outlined,
color: Colors.white60),
activeItem: Icon(Icons.home_filled),
itemLabel: 'Home',
activeColor: Color(0xFF00E5FF), // Cyan
),
BottomBarItem(
inActiveItem: Icon(Icons.search_outlined,
color: Colors.white60),
activeItem: Icon(Icons.search),
itemLabel: 'Search',
activeColor: Color(0xFF00FF7F), // Spring Green
),
BottomBarItem(
inActiveItem: Icon(Icons.favorite_outline,
color: Colors.white60),
activeItem: Icon(Icons.favorite),
itemLabel: 'Favorites',
activeColor: Color(0xFFFF3366), // Pink/Red
),
BottomBarItem(
inActiveItem: Icon(Icons.settings_outlined,
color: Colors.white60),
activeItem: Icon(Icons.settings),
itemLabel: 'Settings',
activeColor: Color(0xFFFFD700), // Gold
),
BottomBarItem(
inActiveItem: Icon(Icons.person_outline,
color: Colors.white60),
activeItem: Icon(Icons.person),
itemLabel: 'Profile',
activeColor: Color(0xFF9D00FF), // Purple
),
],
onTap: (index) {
appController.notchBottomBarController
.jumpTo(index);
switch (index) {
case 0:
appController.scrollToSection(
AppController.section1Key);
break;
case 1:
appController.scrollToSection(
AppController.section2Key);
break;
case 2:
appController.scrollToSection(
AppController.section3Key);
break;
case 3:
appController.scrollToSection(
AppController.section4Key);
break;
case 4:
appController.scrollToSection(
AppController.section5Key);
break;
}
},
),
),
),
);
}),
),
Positioned(
bottom: isDesktop ? 40 : 60,
right: isDesktop ? 40 : 30,
child: Obx(() => AnimatedScale(
duration: const Duration(milliseconds: 500),
scale: !appController.isNavbarVisible.value ? 1.0 : 0.0,
curve: Curves.easeOutBack,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => appController.scrollToTop(),
child: Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: AppConstants.bgColor,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: AppConstants.activeIconColor
.withValues(alpha: 0.5),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: AppConstants.activeIconColor
.withValues(alpha: 0.25),
blurRadius: 15,
spreadRadius: 1,
offset: const Offset(0, 5),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 10,
spreadRadius: -2,
offset: const Offset(0, 8),
),
],
),
child: const Icon(
Icons.arrow_upward_rounded,
color: AppConstants.activeIconColor,
size: 28,
),
),
),
),
)),
),
],
),
),
)),
),
],
),
);
}
Widget _buildSection(GlobalKey key, Widget child) {
return SizedBox(key: key, child: child);
}
}
class _SectionDivider extends StatelessWidget {
const _SectionDivider({required this.isDesktop});
final bool isDesktop;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? AppConstants.spacing64 : AppConstants.spacing24,
vertical: isDesktop ? 80.0 : 40.0,
),
child: isDesktop
? const Divider(color: Colors.white12, thickness: 1)
: const SizedBox.shrink(),
);
}
}
class _MouseHalo extends StatefulWidget {
const _MouseHalo();
@override
State<_MouseHalo> createState() => _MouseHaloState();
}
class _MouseHaloState extends State<_MouseHalo> {
final ValueNotifier<Offset> _mousePos = ValueNotifier(Offset.zero);
@override
void dispose() {
_mousePos.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
opaque: false,
onHover: (event) => _mousePos.value = event.position,
child: ValueListenableBuilder<Offset>(
valueListenable: _mousePos,
builder: (context, pos, child) {
return Transform.translate(
offset: pos + const Offset(-200, -200),
child: child!,
);
},
child: IgnorePointer(
child: Container(
width: 400,
height: 400,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppConstants.primaryColor.withValues(alpha: 0.12),
AppConstants.secondaryColor.withValues(alpha: 0.05),
Colors.transparent,
],
stops: const [0.0, 0.4, 1.0],
),
),
),
),
),
);
}
}
class _DynamicSection extends StatefulWidget {
final Widget child;
final String animKey;
final int index;
const _DynamicSection({
required this.child,
required this.animKey,
required this.index,
});
@override
State<_DynamicSection> createState() => _DynamicSectionState();
}
class _DynamicSectionState extends State<_DynamicSection> {
bool _isVisible = false;
final GlobalKey _key = GlobalKey();
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key(widget.animKey),
onVisibilityChanged: (info) {
if (!_isVisible && info.visibleFraction > 0.1) {
if (mounted) {
setState(() {
_isVisible = true;
});
final String sectionName = widget.animKey;
Get.find<PortfolioController>().trackAction('view_$sectionName');
}
}
},
child: RepaintBoundary(
child: ValueListenableBuilder<double>(
valueListenable: appController.scrollOffset,
builder: (context, scrollOffsetValue, child) {
double tiltAngle = 0.0;
double scale = 1.0;
if (_isVisible && globalScrollController.hasClients) {
final RenderBox? box = _key.currentContext?.findRenderObject() as RenderBox?;
if (box != null) {
final position = box.localToGlobal(Offset.zero);
final screenHeight = MediaQuery.sizeOf(context).height;
final centerY = position.dy + (box.size.height / 2);
if (centerY > -500 && centerY < screenHeight + 500) {
final double distance = ((centerY - (screenHeight / 2)) / (screenHeight / 2)).clamp(-1.0, 1.0);
tiltAngle = distance * 0.03;
scale = 1.0 - (distance.abs() * 0.012);
}
}
}
return Transform(
key: _key,
transform: Matrix4.diagonal3Values(scale, scale, 1.0)
..setEntry(3, 2, 0.0008)
..rotateX(tiltAngle),
alignment: Alignment.center,
child: child,
);
},
child: _isVisible
? RepaintBoundary(child: _applyUniqueAnimation(widget.child, widget.index))
: const SizedBox.shrink(),
),
),
);
}
Widget _applyUniqueAnimation(Widget child, int index) {
final anim =
child.animate().fadeIn(duration: 800.ms, curve: Curves.easeOut);
switch (index % 4) {
case 0:
return anim
.slideX(begin: -0.4, end: 0, curve: Curves.easeOutBack)
.scale(begin: const Offset(0.7, 0.7), curve: Curves.easeOutBack)
.shimmer(duration: 1200.ms, color: Colors.white24);
case 2:
return anim
.slideX(begin: 0.4, end: 0, curve: Curves.easeOutBack)
.scale(begin: const Offset(0.7, 0.7), curve: Curves.easeOutBack)
.shimmer(duration: 1200.ms, color: Colors.white24);
case 1:
case 3:
default:
return anim
.slideY(begin: 0.4, end: 0, curve: Curves.easeOutBack)
.scale(begin: const Offset(0.8, 0.8), curve: Curves.easeOutBack);
}
}
}
Cross-Platform Support #
This package is built with 100% pure Dart and Flutter. No native configuration required.
| Platform | Notch Bar Visible | Notes |
|---|---|---|
| โ Android | Yes (mobile width) | API 21+ |
| โ iOS | Yes (mobile width) | iOS 12+, respects SafeArea |
| โ Web | Yes (narrow) / Top Nav (wide) | Responsive breakpoint at 1100px |
| โ Windows | Yes (narrow) / Top Nav (wide) | Desktop layout auto-applies |
| โ macOS | Yes (narrow) / Top Nav (wide) | Desktop layout auto-applies |
| โ Linux | Yes (narrow) / Top Nav (wide) | Desktop layout auto-applies |
Troubleshooting #
flutter pub get fails #
flutter clean && flutter pub get
iOS Pod install fails #
cd ios && pod deintegrate && pod install
Web build fails with JS errors #
flutter build web --release --dart2js-optimization=O4
flutter run shows "No devices found" #
# Check connected devices
flutter doctor
flutter devices
Resources #
- ๐ฆ Package on pub.dev
- ๐ป Source Code on GitHub
- ๐ Full API Documentation
- ๐ Report an Issue
- ๐ Live HTML Demo
