This is a small tutorial on creating Neumorphic interface elements using Flutter. Here’s the end result.
Also in this tutorial:
- CSS code from 2013
Containeris a cake
- Metal processing tools
- Unnecessary use of Dart 2.7 extensions
- Quite a few ugly screenshots
- A lot of Space!
Neumorphic design is one of the trends which started getting traction in late 2019.
This visual style itself is not completely new, as you can find samples of interfaces utilising it dating much earlier.
However, I am sure that it wasn’t the first one, as the effect was known in the Flash era, as well as used in graphical designs before capabilities to draw soft shadows in realtime emerged.
Despite being known for a while, the term itself was coined relatively recently. It was done by Michal Malewicz, who’s exploring different UI trends and implications of using them.
Typical Neumorphic interface looks like this:
You can also find a lot of experimentation with neuomorphic interfaces on Dribbble. You can safely expect to see this visual style everywhere very soon, just like it was with previous design trends.
The central point of Neumorphism are shadows, which are giving the interface this feel of a surface carefully carved with a spherical drill.
The recipe itself is very simple, just take your drill bit and work on all those corners in your interface.
Please, note the slight curve of the top of the “neumorphized” piece. That’s what gives it that smooth gradient on top.
You can play with the visual effect a bit more right in your browser, just visit the https://neumorphism.io.
- Performance, shadows are expensive. Especially for animation.
- Function over Form, there are no pragmatic reasons to employ this design style, except visual appearance.
However, it won’t hurt to add another expressive instrument to your visual toolset!
Flutter has great support for all the integral parts of Neumorphic UI: Shadows and Gradients.
Let’s start with scaffolding a new Flutter project:
Then, open this new project in your IDE of choice (I mean VS code, of course). Let’s get rid of everything in
main.dart and replace it with the following.
Many good things (and bad ones too) in Flutter starts with a
Container. Let’s add one here too, we’ll figure out if that’s for good or bad a bit later.
Container in reality is just an orchestrator for many other useful widgets to work together to create a convenient way to draw 2D panels. That is, if you’ll visit
Container.build implementation, you’ll see that
Container without any properties renders just… its child! Here’s an approximation of how it looks:
So, in future, as a matter of optimization, you may look at using some of those
Box implementations directly.
We’re not that concerned with this now, so let’s render something fancy. Let’s render a bevel.
After it’s possible to define bevel size, let’s add a
border to our
And… It renders as…
We’ll get there, I promise. This iconic look is described with the following simple shape.
So let’s take our spherical drill and remove some material from it!
Which will look as following, when code is added to the app.
And here we can see one very prominent limitations of Neumorphic design. You can only use it in low-contrast environments. That is, you can’t imitate physically correct soft emboss on an absolutely white or absolutely black surface. So you will always have to choose something in between. It also has some limitations on the accessibility of your visual language. As you can’t have all these barely noticeable touches being an integral part of the design. It simply won’t work for everyone.
To avoid this issue, let’s add some background to our
blueGrey here, but please feel free to choose anything you’re up to. Pastel palette would probably work the best, but not choose anything too bright or too dark.
Well, at least we can see our counter shadow (light) now. The reason why it looks so ugly now is simply because your 🧠 knows how shadows supposed to look on real objects and it doesn’t look like one.
The first thing to fix is color. Shadows are not black. And the lights are not white. That’s probably one of the widest known secrets in design. They are instead relative to the surrounding surface color.
Two simple rules are:
- Shadows a tone darker than background
- Lights are a tone lighter than the background
The gotcha here is that now
BoxShadows needs to be aware of the colour they have been drawn on top of.
Or does it?
On the left is a tonal shadow, on the right is monochrome. I’m going to stick to the first one.
To implement one, we need a couple of small additions.
I do prefer excessive use of
extensions for adding new functionality to existing entities when using Dart. Even for the simplest tasks which already covered by the framework. So here you have it.
It could be used as
Colors.blue.mix(Colors.red, .5). If you don’t like how it looks, you could stick to using
Let’s make use of awesome
Theme support in Flutter and make our
NeumorphicContainer support to be drawn on a standard
For that, let’s define the background color behavior, in
And let’s add
color property to
NeumorphicContainer. Which would fetch its value from
ThemeData but support overrides where needed. Always leave your users a way to override defaults. Especially if you’re the future user of the code you’re writing.
The only thing left is to mix this
Colors.black to this
color and enjoy tonal shadows.
You may see that I’ve used different values for
black here. This disproportion would shift depending on
luminosity of the
dart:ui have a handy
computeLuminance which returns a unit luminosity (value between 0 and 1) for a given color and could be used to programmatically adjust the dominant shadow.
Let’s move on. Next step is to apply our Spherical drill bit to the top of this surface.
Neumorphic design allows a couple of options here: convex and concave. That difference determines how lighting will be applied to our surface.
Let’s start with a latter one and a grain of salt. Flutter doesn’t support inset shadows. At least for now. So we’ll have to go with Gradients, for now.
Which looks as following.
Alright. Let’s check what we got.
- Direct light shadow. Present.
- Counter light reflection. Present.
- Concave surface shape. Present.
But it still looks ugly!
Widely known design secret #2. When something looks ugly but should look nice, just add space.
Space could fix a bad design and turn good design to be a great one. Also, space is what most of the universe consists of, taking into account that the radius of an atom is more than 10,000 times more than the radius of its nucleus. That’s right, only 0.0001% of the universe’s matter is actually something solid. So don’t try to go against that universal law of nature. Just add some space.
With that and some text color tweaks, our surface looks light years better immediately.
We can now also make gradient a little bit more complex, to adapt for this pill shape.
The final push would be to make our container respond to touch gestures. To do so, let’s sprinkle some
Listener on top of the
Container. And convert
NeumorphicContainer to be a
StatefulWidget, as it’ll now have
To the users of VS Code, there’s a very convenient helper. Just place text caret into the widget class name and hit
Cmd/Ctrl + . , or
Cmd/Ctrl + Shift + R` or right click the class name and choose “Refactors”. That’ll call a context menu with “Convert to StatefulWidget” option. Very convenient!
After that, we can wrap
Listener , using another automatic refactor,
wrap with widget.
A productive developer is a happy developer. So know your tools to be happy!
And pressing our container now… Does nothing! We need to use this
We could do that by conditionally adding or removing
boxShadow depending on if
_isPresset is set.
I mean, “Voila, our ugly interaction is ready”. Let’s make it nicer.
Making things nicer #1.
After button is pressed, it’s surface is squished, so some interactivity is expected. Let’s change our gradient look depending on
You can tweak these color variation to your liking as per effect you’re trying to achieve.
Making things nicer #2.
It’s way too snappy. Not that users don’t love snappy interfaces, but this one might be too much. With Flutter, it’s extremely easy to fix this. We literally need just one new line of code. Just swap
AnimatedContainer and add a required
duration param. 150ms would feel pretty snappy still, but you can tweak that as per your preference. Just don’t make it less than 0, otherwise, your Dart VM instance will explode.
And we’re done, a simple, yet quite alive sample of Neumorphic UI is ready!
A complete code example is available in this gist.
If you found this article interesting, you may want to check out a few other things I wrote about Flutter.
- Creating Particle effects for your Flutter application
- Procedurally generated brushed metal textures in Flutter
- Procedurally generated textures in Flutter in general
Please don’t hesitate to write to me to email@example.com, I’m always happy to hear from people reading my articles. Even if the only reason is to tell me how wrong am I.
Just remember, a little bit of space could fix a lot of things!👴🏻