Create a squishy, responsive, cyberpunk button in Flutter with the responsive_styled_widget package

Wenkai Fan
ITNEXT
Published in
5 min readFeb 28, 2021

--

A responsive, cyberpunk button

Do you want to quickly create a button like this in Flutter? It has rounded corners, four different borders with four gradient fill, a gradient shadow, text with varying letter spacing and shadows. Besides, the button is actually responsive, with an automatic height and a width that evaluates to max(50% of the screen width, 500px). If you move a mouse pointer over it, the cursor will change to the click gesture as well. Here are some of the steps I could think of to achieve this using Flutter out of the box.

  1. Calculate the width is not difficult. Get the screen width using MediaQuery and do some simple math will get you there.
  2. The four borders can be drawn using four CustomPainters with gradient fills. The rounded corner can be achieved using the Container widget with rounded corners. The text position needs to shift after clicking so you need to calculate the shift as well.
  3. Current Flutter shadows only use a single color, so to achieve this gradient shadow, we can stack a gradient container with ImageFiltered blurring under the button.
  4. Use a MouseRegion widget to achieve the mouse cursor change.

Using CSS, you can achieve this button style more easily, as CSS supports responsive length units, rounded corners plus different colored borders, gradient shadow with pseudo selector and blur, etc. What if we create a single widget that can easily achieve all those features in Flutter?

First, let's deal with the responsiveness. I suggest you check out this article where I introduced the dimension package. In short, this package lets you define various responsive distance measures.

///10% of the screen width
var length = 10.toVWLength;
///maximum of two lengths
length = Dimension.max(10.toVWLength, 100.toPXLength);
///nested expression with clamp, min, addition of lengths
length = Dimension.clamp(100.toPXLength, 20.toPercentLength + 10.toPXLength, Dimension.min(500.toPXLength, 50.toVWLength));

Using this package, you get precise control of the dimensions you want to use. The widget style is described using the Style class:

class Style {
bool? visible;
double? opacity;

Alignment? alignment;

Dimension? width;
Dimension? height;

EdgeInsets? margin;
EdgeInsets? padding;

BoxDecoration? backgroundDecoration;
BoxDecoration? foregroundDecoration;
List<ShapeShadow>? shadows;
List<ShapeShadow>? insetShadows;
MorphableShapeBorder? shapeBorder;

SmoothMatrix4? transform;
Alignment? transformAlignment;

Alignment? childAlignment;
DynamicTextStyle? textStyle;
TextAlign? textAlign;

SystemMouseCursor? mouseCursor;
}

It is pretty similar to the parameters that the Container widget use. Then you can use the StyleContainer widget to construct the final widget:

Widget widget = StyledContainer(
style: style,
child: Text("Hello World")
);

There is also the AnimatedStyledContainer that will smoothly animate between different styles you provide, similar to the AnimatedContainer widget.

Widget widget = GestureDetector(
onTapDown: (TapDownDetails details) {
setState(() {
toggleStyle = false;
});
},
onTapUp: (TapUpDetails details) {
setState(() {
toggleStyle = true;
});
},
child: AnimatedStyledContainer(
duration: Duration(milliseconds: 100),
style: toggleStyle ? beginStyle : endStyle,
child: Text("Hello World")),
);

When toggling between the beginning style and the end style, AnimatedStyledContainer will smooth animate between them. Notice since the style can be a style map that maps to different styles under different screen sizes, the animation may also trigger if you resize the app window. This is what is used to achieve the neon button at the beginning.

So how does the style look like for this button? It looks like this:

It may look a little bit long at first glance, but most of the code is attributed to configure the four borders and the three text shadows this button uses.

Styles with Transformation

Left: Rotate transform. Right: inner shadow glow

You can also plug in transformations in the style using the SmoothMatrix4 class. It has an almost identical interface to the Matrix4 class that Flutter uses to describe transformations, but it only allows transformations that can be smoothly animated (scaling, translation, and rotation). Also, it uses the Dimension class to describe the translation, so you can write something like:

SmoothMatrix4()..translate(-100.toVWLength)

to ensure your animation always starts outside the screen no matter what screen size you have.

Style serialization/deserialization

If you have followed my past articles, you know the UI packages I published all support serialization. And the reason for doing that is for this package(called responsive_styled_widget). The neon button style can be easily serialized into a JSON string by calling:

style.toJson();

In fact, the style map can also be serialized:

styles.toJson();

To get a style/style map back, use this function:

dynamic? parsePossibleStyleMap(Map<String, dynamic>? style);

which will give you a single style or a style map that you can just plug into the StyledContainer widget.

What’s the Point?

You may think this style sheet thing is against the composition ideology of Flutter and takes us back to CSS, and I would agree with you. But I would argue that this style sheet is mainly a composition of three widely used Flutter widgets: Container, DefaultTextStyle, and Transform. What I did is add responsiveness (through the Dimension class and the ScreenScope class), gradient coloring for the border and shadows, shape morphing and serializability. The Style class covers most of the UI styles you would set but the power of Flutter in creating custom UI is not hindered by using this package. If you don’t like the idea of a style sheet, you can still use the dimension package and the morphable_shape package that is used in this package to create responsive distances and various shape borders.

Some extra use cases I can think of for this package is:

  1. A CSS-style converter. This package supports various CSS styles like margin, padding, width, height, background decoration(color, gradient, image), border styling, shadows, text styles, transformations. It also supports @media rules and dimension calculations like (calc, min, max, clamp). It should be fairly easy to write a converter that can convert some complex CSS styles into Flutter.
  2. A UI library. You can generate a list of common UI styles that you want to use and save them in JSON, like what tailwind css is doing. Then you can reuse those styles anywhere in your app. Animate those styles is easy, as you have the AnimatedStyledContainer widget at hand (or use explicit animations). Modifying those styles is also simple, as the Style class provides the copyWith and merge method for you to generate new styles quickly.
  3. If you want to be extreme, you can actually use style across the framework, giving each Widget an id, a class, or other selectors, and use a state management framework to provide all the styles. The Dart code then just serves the functionalities of javascript.

And that is all for today's article. Thank you!

--

--

Ph.D. in Nuclear Physics, M.S. in Computer Science at Duke University, Flutter lover