Neumorphism Animation or Style is a popular design trend that has been gaining a lot of attention recently. It involves creating user interface elements that appear to be raised from the background and cast a slight shadow, giving them a three-dimensional look. In this article, we will be looking at how to create a Neumorphic animated button in Flutter using the Dart programming language. Even if you’re new to Flutter or haven’t worked with Neumorphism before, don’t worry! We’ll take a step-by-step approach to building the button and explain the code along the way.
If you want to clear the basics of how Neumorphism works, you can check out our previous article on the topic and then come back to understand this code in more detail.
Implementing a Neumorphic Animation in Flutter
Creating a Neumorphism Button
First, define a custom widget class called NeumorphismAnimatedButton
that extends StatefulWidget
. This class has a state class _NeumorphismAnimatedButtonState
that extends State
and is responsible for building the user interface.
class NeumorphismAnimatedButton extends StatefulWidget {
const NeumorphismAnimatedButton({Key? key}) : super(key: key);
@override
State<NeumorphismAnimatedButton> createState() => _NeumorphismAnimatedButtonState();
}
class _NeumorphismAnimatedButtonState extends State<NeumorphismAnimatedButton> {
Widget build(BuildContext context) {
return ///your User Interface
}
}
Inside the _NeumorphismAnimatedButtonState
class, there are several variables that keep track of the state of the button. These include isClicked
and isDarkMode
, which stores whether the button has been clicked and whether the app is currently in dark mode, respectively. There are also variables to define colors and offsets used for the button’s styling.
class NeumorphismAnimatedButton extends StatefulWidget {
const NeumorphismAnimatedButton({Key? key}) : super(key: key);
@override
State<NeumorphismAnimatedButton> createState() => _NeumorphismAnimatedButtonState();
}
class _NeumorphismAnimatedButtonState extends State<NeumorphismAnimatedButton> {
// state variables
double turns = 0.0;
bool isClicked = false;
bool isDarkMode = true;
Color customWhiteColor = Colors.white60;
Color customBlackColor = Colors.black54;
Widget build(BuildContext context) {
return ///your User Interface
}
}
The _toggleTheme
a function is called when the user toggles the theme switch in the app bar. This function toggles the isDarkMode
variable and updates the UI using the setState
function.
void _toggleTheme() {
setState(() {
isDarkMode = !isDarkMode;
});
}
The build
function returns a Scaffold
widget with an app bar and a body that consists of a centered Stack widget. The Stack widget contains a Padding widget with a AnimatedContainer
widget inside. The AnimatedContainer
widget is wrapped with a GestureDetector
widget that listens for taps on the button.
Scaffold(
backgroundColor: isDarkMode?const Color(0xff2e3239):backgroundColor, // use ternary operator to set background color based on current theme
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/images/sitejuggler.png", // add an image to the app bar
height: 50,width: 50,),
Text("SiteJuggler"), // add app title
],
),
actions: [
Switch( // add a switch button to toggle theme
value: isDarkMode,
onChanged: (value) {
_toggleTheme();
},
activeColor: Colors.white,
inactiveThumbColor: Colors.white,
),
],
),
body: Center(
child: Stack(
children:[
Padding(
padding: const EdgeInsets.all(30.0),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
child: GestureDetector(
onTap: (){
},
child: FittedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: isDarkMode?const Color(0xff2e3239):backgroundColor, // use ternary operator to set button color based on current theme
boxShadow:[
BoxShadow(
blurRadius: blur,
offset: Offset(20,-20),
color: isDarkMode?const Color(0xff35393f):Colors.white, // use ternary operator to set shadow color based on current theme
),
BoxShadow(
blurRadius: blur,
offset: Offset(-20,-20),
color: isDarkMode?const Color(0xff23262a):Colors.grey, // use ternary operator to set shadow color based on current theme
)
]
),
child: SizedBox(
height: 100,
width: 100,
),
),
),
),
),
),
],
),
),
);
The button is styled using a BoxDecoration
that adds a border-radius, background color, and two BoxShadow
widgets to create a Neumorphic effect. The color and offset of the shadows change depending on whether the app is in dark mode or not. The height and width of the button are set using a SizedBox
widget.
class NeumorphismAnimatedButton extends StatefulWidget {
const NeumorphismAnimatedButton({Key? key}) : super(key: key);
@override
State<NeumorphismAnimatedButton> createState() => _NeumorphismAnimatedButtonState();
}
class _NeumorphismAnimatedButtonState extends State<NeumorphismAnimatedButton> {
// state variables
double turns = 0.0;
bool isClicked = false;
bool isDarkMode = true;
Color customWhiteColor = Colors.white60;
Color customBlackColor = Colors.black54;
// function to toggle theme between light and dark mode
void _toggleTheme() {
setState(() {
isDarkMode = !isDarkMode;
});
}
@override
Widget build(BuildContext context) {
const backgroundColor = Colors.white70; // set a constant background color for light mode
Offset distance = const Offset(30,30);
Offset distance2 = const Offset(-30,30);
double blur = 40.0;
return Scaffold(
backgroundColor: isDarkMode?const Color(0xff2e3239):backgroundColor, // use ternary operator to set background color based on current theme
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/images/sitejuggler.png", // add an image to the app bar
height: 50,width: 50,),
Text("SiteJuggler"), // add app title
],
),
actions: [
Switch( // add a switch button to toggle theme
value: isDarkMode,
onChanged: (value) {
_toggleTheme();
},
activeColor: Colors.white,
inactiveThumbColor: Colors.white,
),
],
),
body: Center(
child: Stack(
children:[
Padding(
padding: const EdgeInsets.all(30.0),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
child: GestureDetector(
onTap: (){
},
child: FittedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: isDarkMode?const Color(0xff2e3239):backgroundColor, // use ternary operator to set button color based on current theme
boxShadow:[
BoxShadow(
blurRadius: blur,
offset: Offset(20,-20),
color: isDarkMode?const Color(0xff35393f):Colors.white, // use ternary operator to set shadow color based on current theme
),
BoxShadow(
blurRadius: blur,
offset: Offset(-20,-20),
color: isDarkMode?const Color(0xff23262a):Colors.grey, // use ternary operator to set shadow color based on current theme
)
]
),
child: SizedBox(
height: 100,
width: 100,
),
),
),
),
),
),
],
),
),
);
}
}
well, we have set up the basic structure and styling of a Neumorphic animated button that can be customized further to fit specific design needs.

Let’s Begin With The Animation
Creating Animated Icon
first We add the Center widget to position the AnimatedIcon widget at the center of the screen.
Then we add the AnimatedIcon widget with the following properties:
- The
icon
property is set toAnimatedIcons.menu_close
, which is an AnimatedIconData object that displays the menu icon when the button is in its default state, and animates into a close icon when the button is clicked. - The
progress
property is set to_controller
, which is aAnimationController
object that controls the animation of the icon. We’ll update this controller when the button is clicked to start the animation. - The
size
property is set to 20, which sets the size of the icon. - The
color
property is set toisDarkMode? Colors.white:Colors.black
, which sets the color of the icon based on the value ofisDarkMode
. IfisDarkMode
istrue
, the icon color will be white, otherwise it will be black.
Center(
child: AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: _controller,
size: 20,
color: isDarkMode? Colors.white:Colors.black,
)
)
To create animations, we need to use an AnimationController. In this code, we are defining a variable _controller
of type AnimationController
, which is used to control the animation.
late AnimationController _controller;
The late
keyword is used to tell the Dart compiler that the variable may be initialized at a later time. We are initializing the _controller
variable in the initState
method using the vsync
argument, which ensures that the animation is synchronized with the device’s vertical sync.
@override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync:this
);
super.initState();
}
We are also disposing of the controller in the dispose
method to free up resources when the widget is removed from the tree.
@override
void dispose() {
_controller.dispose();
super.dispose();
}

Implementing Animated Rotation
- We use the
AnimatedRotation
widget to animate the rotation of the button when clicked. This widget takes three properties:curve
,turns
, andduration
. - The
curve
property specifies the easing curve for the animation, which determines how fast the animation starts and ends. - The
turns
property specifies how many turns the button should rotate, which is set to a quarter turn or 1/4. - The
duration
property specifies how long the animation should take, which is set to one second. - We wrap the button with a
GestureDetector
a widget that listens for taps on the button. - When the button is tapped, the
onTap
function is called, which checks whether the button has been clicked or not using theisClicked
variable. - If the button has been clicked, we set the
turns
variable to rotate the button back a quarter turn using-1/4
, and we call_controller.reverse()
to reverse the rotation animation. - If the button has not been clicked, we set the
turns
variable to rotate the button forward a quarter turn using1/4
, and we call_controller.forward()
to start the rotation animation. - Finally, we set the
isClicked
variable to the opposite of its current value to toggle the button state. - The button is styled using a
Container
a widget that has aBoxDecoration
with a rounded border and twoBoxShadow
widgets to create the Neumorphic effect. Thecolor
property of the container depends on whether the app is in dark mode or not. - The
height
andwidth
of the button are set using aSizedBox
widget. - The
Center
widget contains anAnimatedIcon
a widget that displays the menu icon, which is animated using the_controller
variable. Thecolor
property of the icon depends on whether the app is in dark mode or not.
AnimatedRotation(
curve: Curves.easeOutExpo, // An easing curve that starts fast and then gets slower at the end
turns: turns, // The amount of rotation (in terms of number of turns)
duration: const Duration(seconds: 1), // The duration of the rotation animation
child: GestureDetector(
onTap: (){
if(isClicked){ // If the button is clicked
setState(() {
turns -= 1/4; // Reduce the rotation by one quarter
_controller.reverse(); // Reverse the rotation animation
});
}
else{ // If the button is not clicked
setState(() {
turns += 1/4; // Increase the rotation by one quarter
_controller.forward(); // Start the rotation animation
});
}
isClicked = !isClicked; // Toggle the button clicked state
},
child: FittedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: isDarkMode ? const Color(0xff2e3239) : backgroundColor,
boxShadow:[
BoxShadow(
blurRadius: blur,
offset: -distance,
color: isDarkMode ? const Color(0xff35393f) : Colors.white,
),
BoxShadow(
blurRadius: blur,
offset: distance,
color: isDarkMode ? const Color(0xff23262a) : Colors.grey,
)
]
),
child: SizedBox(
height: 100,
width: 100,
child: Center(
child: AnimatedIcon(
icon: AnimatedIcons.menu_close, // The icon to display
progress: _controller, // The progress of the rotation animation
size: 20,
color: isDarkMode ? Colors.white : Colors.black, // The color of the icon
)
),
),
),
),
),
),
Creating Neumorphism Animation Page
We are now adding another AnimatedContainer that will be shown when the button is clicked. This container will cover the whole screen with a background color and will be hidden when the button is not clicked.
// AnimatedContainer to show/hide the full screen background when button is clicked
AnimatedContainer(
height: isClicked ? MediaQuery.of(context).size.height : 0,
width: isClicked ? MediaQuery.of(context).size.width : 0,
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: isDarkMode ? const Color(0xff2e3239) : backgroundColor, // set background color based on dark mode
boxShadow:[
BoxShadow(
blurRadius: blur,
offset: -distance,
color: isDarkMode ? const Color(0xff35393f) : Colors.white, // set shadow color based on dark mode
),
BoxShadow(
blurRadius: blur,
offset: distance,
color: isDarkMode ? const Color(0xff23262a) : Colors.grey, // set shadow color based on dark mode
)
]
),
),
Let’s Fix the Shadow Problem
In the previous code, we were using two box shadows to create the shadow effect on the button. But when we rotate the button, the shadow also rotates, which may not look visually appealing. So, to solve this problem, we will use different shadow offsets for when the button is clicked or not.
We have created two different shadow offsets: distance
and distance2
. distance
is the shadow offset when the button is not clicked, and distance2
is the shadow offset when the button is clicked. We use a ternary operator to determine which shadow offset to use based on whether the button is clicked or not.
Overall, this gives us a nice shadow effect that remains consistent even when the button is rotated.
boxShadow:[
BoxShadow(
blurRadius: blur,
offset: isClicked ? distance2 : -distance,
color: isDarkMode ? const Color(0xff35393f) : Colors.white,
),
BoxShadow(
blurRadius: blur,
offset: isClicked ? -distance2 : distance,
color: isDarkMode ? const Color(0xff23262a) : Colors.grey,
)
]
Here, the blurRadius
is the amount of blur applied to the shadow, offset
is the distance the shadow is offset from the button, and color
is the color of the shadow.
We use the same isClicked
ternary operator to determine which color to use for the shadows based on whether the app is in dark mode or not.
Output
As you have implemented the code and run it, you should see a button with the animated menu icon. When you click on the button, the icon rotates and expands the container, which covers the whole screen, displaying some dummy text. When you click the button again, the container shrinks and the icon rotates back to its original position. The button also changes color to reflect the dark or light mode based on the device setting. Overall, the output should show a simple but effective implementation of the Neumorphism style in a Flutter app.
Full Source Code
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
debugShowCheckedModeBanner: false,
title: 'Stepper Widget- SiteJuggler',
home: const NeumorphismAnimatedButton(),
);
}
}
class NeumorphismAnimatedButton extends StatefulWidget {
const NeumorphismAnimatedButton({Key? key}) : super(key: key);
@override
State<NeumorphismAnimatedButton> createState() => _NeumorphismAnimatedButtonState();
}
class _NeumorphismAnimatedButtonState extends State<NeumorphismAnimatedButton>
with TickerProviderStateMixin { ///new
late AnimationController _controller;///new
double turns = 0.0;
bool isClicked = false;
bool isDarkMode = true;
Color customWhiteColor = Colors.white60;
Color customBlackColor = Colors.black54;
void _toggleTheme() {
setState(() {
isDarkMode = !isDarkMode;
});
}
@override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync:this
);
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const backgroundColor = Colors.white70;
Offset distance = const Offset(30,30);
Offset distance2 = const Offset(-30,30);
double blur = 40.0;
return Scaffold(
backgroundColor: isDarkMode?const Color(0xff2e3239):backgroundColor,
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/images/sitejuggler.png",
height: 50,width: 50,),
Text("SiteJuggler"),
],
),
actions: [
Switch(
value: isDarkMode,
onChanged: (value) {
_toggleTheme();
},
activeColor: Colors.white,
inactiveThumbColor: Colors.white,
),
],
),
body: Center(
child: Stack(
children:[
Padding(
padding: const EdgeInsets.all(90.0),
child: AnimatedContainer(
height: isClicked?MediaQuery.of(context).size.height:0,
width: isClicked?MediaQuery.of(context).size.width:0,
//color: customBlackColor,
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: isDarkMode?const Color(0xff2e3239):backgroundColor,
boxShadow:[
BoxShadow(
blurRadius: blur,
offset: -distance,
color: isDarkMode?const Color(0xff35393f):Colors.white,
),
BoxShadow(
blurRadius: blur,
offset: distance,
color: isDarkMode?const Color(0xff23262a):Colors.grey,
)
]
),
),
),
Padding(
padding: const EdgeInsets.all(30.0),
child: AnimatedContainer(
height: isClicked?50:100,
width: isClicked?50:100,
duration: Duration(milliseconds: 200),
child: AnimatedRotation(
curve: Curves.easeOutExpo, //starts fast and then get slow at end
turns: turns,
duration: const Duration(seconds: 1),
child: GestureDetector(
onTap: (){
if(isClicked){
setState(() {
turns -= 1/4; // this is one quarter of rotation
// turns = turns - 1/4;
_controller.reverse(); ///new
});
}
else{
setState(() {
turns += 1/4; // this is one quarter of rotation
// turns = turns + 1/4;
_controller.forward(); ///new
});
}
isClicked = !isClicked;
},
child: FittedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: isDarkMode?const Color(0xff2e3239):backgroundColor,
boxShadow:[
BoxShadow(
blurRadius: blur,
offset: isClicked?distance2:-distance,
color: isDarkMode?const Color(0xff35393f):Colors.white,
),
BoxShadow(
blurRadius: blur,
offset: isClicked?-distance2:distance,
color: isDarkMode?const Color(0xff23262a):Colors.grey,
)
]
),
child: SizedBox(
height: 100,
width: 100,
child: Center(
child: AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: _controller,
size: 20,
color: isDarkMode? Colors.white:Colors.black,
)
),
),
),
),
),
),
),
),
],
),
),
);
}
}
Conclusion
In this article, we learned how to create a beautiful Neumorphic Animation menu button in Flutter. We started by explaining what Neumorphism is and how it can be used in UI design. Then, we walked through the code step by step, adding a menu icon with animation and implementing a Neumorphic effect. We also added an expandable container that appears when the button is clicked. Finally, we solved the issue of the shadow turning along with the button. By following this tutorial, you should now have a good understanding of how to create Neumorphic UI elements in Flutter.