Neumorphism in Flutter

Tony Owen
5 min readMar 14, 2021

--

Neumorphism has been around for a while now and seemingly quite popular. It’s been a while since I last tried a design challenge, I was looking through dribbble and found this interesting design…

So lets make it (or very close to it)… TL;DR give me the code: https://github.com/tunitowen/Flutter-Dark-Neumorphism

Let’s ignore the header, it’s just text and and image. We’ll focus on the dial, and the cards at the bottom.

Dial

To breakdown the dial we have:
An inset circle, a raised inner circle, and a progress ring in between

Inset Circle

Flutter doesn’t have an inner shadow (that I could find anyway), so we have to be a little creative here. In order to get the effect (shadow top left, highlight bottom right) we’re going to play with RadialGradients and ClipPaths.

There will be a few layers to this component, to do this we’ll use the Stack widget and add the following:

  1. A circle Container the same color as our background
  2. A ClipPath containing a circular Container with a radial gradient for the highlight
  3. A ClipPath containing a circular Container with a radial gradient for the shadow

We can create a RadialGradient that looks like an inner shadow by playing around with the radius, focalRadius, center, and focal properties… this was all trial and error on my part. But, we only want that shadow in the top left; to achieve this we can use a ClipPath to only show that corner as shown below…

Here’s the StatelessWidgets that I ended up with…

class CircleInnerShadow extends StatelessWidget {

final Color shadowColor;
final Color backgroundColor;

const CircleInnerShadow(
{Key key, @required this.shadowColor, @required this.backgroundColor})
: super(key: key);

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
backgroundColor,
shadowColor,
],
center: AlignmentDirectional(0.05, 0.05),
focal: AlignmentDirectional(0, 0),
radius: 0.5,
focalRadius: 0,
stops: [0.75,
1.0],
),
),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0, 0.45],
colors: [backgroundColor.withOpacity(0), backgroundColor])),
),
);
}
}

class ShadowClipper extends CustomClipper<Path> {
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return true;
}

@override
Path getClip(Size size) {
Path path = Path();
path.moveTo(0, 0);
path.lineTo(size.width, 0);
path.lineTo(0, size.height);
path.close();
return path;
}
}

and to use them together…

ClipPath(
clipper: ShadowClipper(),
child: CircleInnerShadow(
shadowColor: shadowColor,
backgroundColor: backgroundColor,
),
)

The same, but inverse, for the highlight can be found in the git repo.

At this point we have something that looks like this…

Lets move on to the inner raised circle. Flutter has us covered here, as a Container can take an array of BoxShadow

Container(
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: highlightColor,
offset: Offset(-10, -10),
blurRadius: 20,
spreadRadius: 0),
BoxShadow(
color: shadowColor,
offset: Offset(10, 10),
blurRadius: 20,
spreadRadius: 0)
]
))

Above we simple supply two shadows, one -10, -10 to move top left. One 10, 10 to move bottom right. Once we add that in, we’re left with this…

Starting to look something like what we want.

Time for some color, the designer has chosen to have a colorful progress ring. To do this we’ll use a CustomPainter . We need to draw an arc, and the Paint needs to have a vertical LinearGradient here’s the StatelessWidget and CustomPainter

import 'package:vector_math/vector_math.dart' as vm;class ProgressRing extends StatelessWidget {
final double progress;

const ProgressRing({Key key, @required this.progress}) : super(key: key);

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return SizedBox.expand(
child: CustomPaint(
painter: RingPainter(
strokeWidth: constraints.maxWidth * 0.15,
progress: progress)));
});
}
}

class RingPainter extends CustomPainter {
final double strokeWidth;
final double progress;

RingPainter({@required this.strokeWidth, @required this.progress});

@override
void paint(Canvas canvas, Size size) {
final inset = size.width * 0.18;

final rect =
Rect.fromLTRB(inset, inset, size.width - inset, size.height - inset);

canvas.drawArc(
rect,
vm.radians(-90),
vm.radians(360 * progress),
false,
Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Constants.gradientStart,
Constants.gradientMiddle,
Constants.gradientEnd
]).createShader(rect)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round);
}

@override
bool shouldRepaint(RingPainter oldDelegate) {
if (oldDelegate.progress != progress ||
oldDelegate.strokeWidth != strokeWidth) {
return true;
}
return false;
}
}

Simply, I created a StatelessWidget give that the progress we need to draw, and then pass that on (along with the relative stroke width) to the RingPainter . The RingPainter draws an arc, setting it’s startAngle to -90 (top of the circle), and the sweep angle to the correct percentage of 360. (Yes I know creating the Paint inside the draw method is bad, but this is a quick hack together, so deal with it 😜 ).

Now we have the finished Circle…

We’re done with the circle, on to the cards at the bottom of the screen…

These are really straightforward, as we’ll make use of the Container and array of BoxShadow

Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
color: Constants.backgroundColor,
boxShadow: [
BoxShadow(
color: Constants.softHighlightColor,
offset: Offset(-10, -10),
spreadRadius: 0,
blurRadius: 10),
BoxShadow(
color: Constants.softShadowColor,
offset: Offset(10, 10),
spreadRadius: 0,
blurRadius: 10)
]))

Above I create a Container , again give two shadows (-10,-10 & 10,10), and give the Container rounded corners. Set the color of the Container to be the same as your background color, and we have something like this…

For the circle inside this view, I reused the circle code from above, checkout the git repo to see exactly whats going on. Throw some text and icons in and we have …

So thats it, done.

iPhone portrait

I added a bit of responsiveness as I went along. For example, the circle and progress ring are all relative in size, so this component scales well. I also added a layout change for landscape …

iPhone landscape

Of course this is Flutter, so here is it on the mobile web…

Mobile Web

and stretched out for desktop…

Desktop Web

--

--