Shadows and Neumorphism in Flutter

David Gonzalez
Flutter Community
Published in
8 min readFeb 22, 2021

Recently, I read an article talking about the Neumorphism, a “minimal way to design with a soft, extruded plastic look”. Some of UI Designers comment this design as good aesthetic, but doomed to disappear since there is not enough contrast to identify quickly components, especially for impaired people.

On contrary, the material design uses shadows a lot, in order to highlight even more components.

We will see how to do this in Flutter for any shapes:

  • Text
  • Rectangles and oval widgets
  • Complex shapes

I also tried neumorphism design. Even if I‘m not sure to recommend this, I will show you how to do it.

Me, afraid by all the ways to draw shadows I have to describe.

Text

It’s very simple to add shadows to text. You define it directly through its style property.

Text(
"Hello :)",
style: TextStyle(
fontSize: 60.0,
fontWeight: FontWeight.bold,
color: Colors.blue,
shadows: [
Shadow(
color: Colors.indigo,
offset: Offset(0.0, 3.0),
blurRadius: 3.0),
]),
);

In this example, we defined one indigo colored shadow with a little shift on Y axis. Here is the result.

As you can see, we are able to add several Shadow objects to our text. That’s how we can make a neumorphic text.

Neumorphic text

Neumorphic elements are always composed (at least) of two shadows: one lighter than the widget color, the other a little bit darker.

Widget neumorphism(BuildContext context) {
final elevation = 3.0;
return Text(
"Hello :)",
style: TextStyle(
fontSize: 60.0,
fontWeight: FontWeight.bold,
color: Colors.grey.shade50,
shadows: [
Shadow(
color: Colors.grey.shade300,
offset: Offset(3.0, 3.0),
blurRadius: elevation),
Shadow(
color: Colors.white,
offset: Offset(-3.0, 3.0),
blurRadius: elevation),
]),
);
}

Shadows are placed a little bit shifted from the center, and opposed to each other. The widget color must be very similar to the background color, a little bit lighter, in order to create this “elevation (altitude?) perception”.

The result is very smooth and sweet, but in my opinion, it can make the text hard to read.

Basic shapes

For basic shapes, rectangle and oval (and circles), Flutter sdk provides two widgets: the DecoratedBox, that you can also find it in Container widget (as decoration input parameter), and the PhysicalModel widget. The last one may be used to provide a more realistic look to your widget, and is easier to use than the DecoratedBox.

Using PhysicalModel

One of the good point of this widget is that you only have to specify an elevation to create a shadow, like some other widgets : FloatingActionButton and RaisedButton, AppBar, BottomAppBar, Card, et cetera

return PhysicalModel(
color: Colors.lightBlue,
elevation: 3.5,
shape: BoxShape.rectangle,
child: SizedBox.fromSize(
size: const Size.square(100.0),
),
);

Below some results for different shapes.

It’s also very easy to animate. To do this, you have at your disposal the AnimatedPhysicalModel that will handle the animation controller for you.

return AnimatedPhysicalModel(
child: GestureDetector(
onTap: () => ..., // change elevation here
child: SizedBox.fromSize(
size: const Size.square(100.0),
),
),
shape: BoxShape.rectangle,
elevation: elevation,
borderRadius: BorderRadius.circular(12.0),
color: Colors.amber,
shadowColor: Colors.deepOrange,
duration: Duration(milliseconds: 500),
);

It’s like the AnimatedContainer, but simplest (and less powerful).

Using DecoratedBox

One limitation using PhysicalModel is that you define a unique shadow through the elevation input parameter, with a predefined orientation (offset). Therefore it’s not possible to have a neumorphic shape using PhysicalModel widget. DecoratedBox widget solve this problem, by allowing you to define a list of BoxShadow.

Why not a list of Shadow objects ? Because with BoxShadow you can define the spreadRadius!

BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
shape: BoxShape.rectangle,
color: Colors.lightBlue,
boxShadow: [
BoxShadow(
color: Colors.blue.shade700,
spreadRadius: 1.0,
blurRadius: 10.0,
offset: Offset(3.0, 3.0))
],
);

One difference I noted compared to PhysicalModel is that shadows corners of a rectangle shape will be rounded. It looks strange to me when a rectangle shape has a rounded rectangle shadow.

You want to animate it ? No problem, there is the DecoratedBoxTransition widget. It’s a little bit harder to use it, as it requires you to keep an AnimationController object (so to create a Stateful widget). If you don’t especially need to use a stateful widget, then you can use AnimatedContainer instead.

AnimatedContainer(
duration: Duration(milliseconds: 300),
constraints: BoxConstraints.expand(width: 100.0, height: 100.0),
decoration: decorationsList[index],
child: Icon(Icons.play_arrow, color: Colors.white,),
)

In that example above, I don’t have any Stateful widget. So instead of creating it especially for animating the lelvation of my rectangle shaped widget, I prefer to use the AnimatedContainer, specifying the new decoration to display (decorationsList[index]).

Neumorphic basic shape

That design implies to draw multiple shadows. So as written earlier, PhysicalModel won’t be able to do the job. We can use DecoratedBox (and Container) for that purpose.

BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Colors.grey.shade50,
shape: BoxShape.rectangle,
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
spreadRadius: 0.0,
blurRadius: elevation,
offset: Offset(3.0, 3.0)),
BoxShadow(
color: Colors.grey.shade400,
spreadRadius: 0.0,
blurRadius: elevation / 2.0,
offset: Offset(3.0, 3.0)),
BoxShadow(
color: Colors.white,
spreadRadius: 2.0,
blurRadius: elevation,
offset: Offset(-3.0, -3.0)),
BoxShadow(
color: Colors.white,
spreadRadius: 2.0,
blurRadius: elevation / 2,
offset: Offset(-3.0, -3.0)),
],
);

As you can see in that example, I used four shadows, to create a more powerful opacity gradient.

As you can see in the result picture above, the design is very subtle (I mean sophisticated).

Complex shapes

If you want to add shadows to a more complex shape, as for example a star, then previous widget won’t do the trick. The first solution is PhysicalShape, equivalent to PhysicalModel, except that you can define properly your shape (with a CustomClipper object). The other one is the CustomPaint, more complex to handle, but also more powerful.

Using PhysicalShape

PhysicalShape widget share same limitations as PhysicalModel one: you can only have one shadow, defined by elevation input parameter. Which is a good point too, as it will make shadows homogeneous in your application.

return PhysicalShape(
clipper: StarClipper(),
color: Colors.amber,
elevation: 4.0,
shadowColor: Colors.deepOrange,
child: SizedBox.fromSize(size: Size.square(100.0),),
);

To use this widget, you will have to define your own widget inheriting from CustomClipper<Path>. A Path object is a good way to describe a shape, as you define lines and curves in it.

class StarClipper extends CustomClipper<Path> {

@override
bool shouldReclip(covariant CustomClipper oldClipper) => false;

@override
Path getClip(Size size) => ShapeUtils.createStar(size);

}

In our example, we build our shape in getClip method. Like SVG image format, we will be able to adapt our shape with the allowed space available (size input parameter), and without any quality loss. ShapeUtils.createStar method returns a Path object describing the star we want to draw.

Using CustomPaint

To draw that Path object with multiple shadows, you shall use CustomPaint, as this widget allows you to draw directly onto the Canvas. This Canvas instance, provided by the Flutter engine, makes you able to paint shapes (line, circle, oval, rectangle, Path) and also shadows!

return CustomPaint(
size: Size.square(100.0),
willChange: false,
isComplex: true,
painter: ShadowedShapePainter(
shape: ShapeUtils.createStar(Size.square(100.0)),
shapeColor: Colors.amber,
shadows: [
Shadow(
offset: Offset(2.5, 2.5),
blurRadius: 6.0,
color: Colors.deepOrange.withOpacity(0.5),
)
]),
);

Do not be mistaken, for doing that you will have to create your own CustomPainter class, in order to provide an instance to painter input parameter. In that example, we created the ShadowedShapePainter, reusing Shadow class used in TextStyle (by laziness).

class ShadowedShapePainter extends CustomPainter {
final Path shape;
final Color shapeColor;
final List<Shadow> shadows;
final Paint _paint;

ShadowedShapePainter({this.shape, this.shapeColor, this.shadows})
: _paint = Paint()
..color = shapeColor
..style = PaintingStyle.fill
..isAntiAlias = true;

@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height));
shadows.forEach((s) {
canvas.save();
canvas.translate(s.offset.dx, s.offset.dy);
canvas.drawShadow(shape, s.color, sqrt(s.blurRadius), false);
canvas.restore();
});
canvas.drawPath(shape, _paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

The central method in that class is paint, where I painted shadows first, using Canvas.translate method to perform the shadow offset. Canvas.save method is here to cancel the translation, going back to the last saved state (using Canvas.save). I draw the shape at the end to avoid shadows to be drawn on top of it.

Neumorphic complex shape

I tried to use Canvas.drawShadow method to create a neumorphic star shape. I was unsuccessful as you cannot draw a white shadow using drawShadow method.

So I’ve done this differently: I used a Paint instance instead!

@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height));
shadows.forEach((s) {
_shadowPaint
..color = s.color
..maskFilter = MaskFilter.blur(BlurStyle.normal, sqrt(s.blurRadius));

canvas.save();
canvas.translate(s.offset.dx, s.offset.dy);
canvas.drawPath(shape, _shadowPaint);
canvas.restore();
// We want to avoid Canvas.drawShadow for neumorphism design,
// as it draws a greyed shadow !
//canvas.drawShadow(shape, s.color, sqrt(s.blurRadius), false);
});
canvas.drawPath(shape, _paint);
}

Instead of using Canvas.drawShadow, I painted the shape another time with a Paint instance applying the blur effect.

It’s difficult to have a good result with complex shapes, as you have to play with offsets and shadow blur a lot ! Also, I failed to set a great elevation to that neumorphic star.

Conclusion

There is multiple ways to draw shadows, depending on what you want to achieve. Before knowing PhysicalModel and PhysicalShape widget, I used DecoratedBox a lot. But now I recommend to use PhysicalModel and PhysicalShape, as they guaranty your shadows to be in alignment with other widgets from the Flutter sdk.

Just keep in mind that drawing shadows have a cost in terms of performance and battery consumption.

It’s like spices, use shadows wisely.

--

--

David Gonzalez
Flutter Community

Hi, I’m David, a french mobile applications developer. As a freelance, I work on Android and iOS applications since 2010. I also work on Flutter now !