About 2 years ago, I created a sample card widget for a web dashboard with React JS. I was inspired by the source code on
.card
margin: 0 -8px 32px -8px
color: rgba(0,0,0,.87)
box-shadow: 2px 2px 8px rgba(0,0,0,.05)
I’ve recreated a similar card according to my dashboard needs with React JS. I use an absolute position and a negative value in the top
property.
Look at the icons stacked on top of the cards.
boxicon: {
position: "absolute",
zIndex: 1,
top: -20,
left: 12,
}
In Flutter, we can use margin
on some of the widgets. For example, Container, Card, etc. And the margin
property is implementing the
How we use padding
and margin
in Flutter:
Container(
padding: EdgeInsets.all(8),
margin: EdgeInsets.fromLRTB(4,8,4,2)
)
EdgeInsetsGeometry in Flutter, has an isNonNegative
method. So….
Margin values must be positive, we cannot assign negative values to EdgeInsetsGeometry.
We will receive a failed assertion when we provide negative values to the margins
or padding
properties.
════════ Exception caught by widgets library ════════
The following assertion was thrown building Card(dirty, dependencies: [_InheritedTheme, _LocalizationsScope-[GlobalKey#95d0d]]):
'package:flutter/src/widgets/container.dart': Failed assertion: line 267 pos 15: 'margin == null || margin.isNonNegative': is not true.
To create the card widget, we can make a similar behavior, or create a similar shape.
Here are several ways to achieve this:
Positioned widget | In CSS if you know about position: absolute
. In Flutter we can achieve similar behavior with the Positioned widget.
Transform widget | This is similar to the transform property in CSS. It applies a transformation before the widget is rendered.
CustomPainter | I haven’t tried this, but I think it’s possible to create a custom shape like the card above with the CustomPainter class.
In this article, I will use the Transform class to crate card widgets like the image above. Since a negative margin can’t be applied, we will move the icon widget with matrix translation from the Transform class.
Matrix translation: A type of transformation that occurs when a figure is moved from one location to another on the coordinate plane without changing its size, shape, or orientation… [
With matrix translation, we can move widgets without changing anything except their position. We can prove it by comparing the position of the widget’s coordinates. If you wonder how to get the coordinate of a widget, you can try with extension method globalPaintBound
.
Read my article about it here:
Before we apply the transformation widget, let's create the initial card. (See the picture before translation).
Now we want to place the icon staked above the card. We will use matrix translation. To add the translation, there are 2 options.
If the widget parent is not a Container widget, you can wrap them with Transfrom
the widget. For example:
Transform(
transform: Matrix4.translationValues(x, y, z),
child: const SomeWidget(),
)
If it's a Container, then we can assign it directly to the transform property:
Container(
transform: Matrix4.translationValues(x, y, z),
.....// other props
)
After adding the translation value to the icon widget, now we have a stacked Icon on the card widget (See the picture after translation).
We have done with the shape of the card widget. But if we compare the current result, we missed the shadow. To add the effect shadow we can add it manually to the BoxDecoration property from the Container widget.
We also can use an extension method named addNeumorphism
.
Manual shadow for the icon box
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
offset: Offset(3, 3),
blurRadius: 8,
color: Color(0xFFffa726),
),
BoxShadow(
offset: Offset(-1, -1),
blurRadius: 10,
color: Colors.white,
),
....
Use the extension method neumorphism
@override
Widget build(BuildContext context) {
return Container(
..../// rest of code
).addNeumorphism(
bottomShadowColor: Colors.black26,
);
And now we have a better card with the implemented shadow effect:
The final step we need to do is to create a responsive layout on the card widget. I’ve also shared an article about responsive layouts in Flutter. For more details, you can read it here:
Cards will be displayed with the GridView.count()
widget. Then we apply the responsive method to determine the crossAxisCount
, and the childAspectRatio
value.
...
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: GridView.count(
crossAxisSpacing: 20,
// mainAxisSpacing: 20,
childAspectRatio: size.width /
size.height /
context.responsive(0.3,
md: 0.6,
lg: 0.59,
xl: 1.2), // this ratio is hard code, you may need to change it base on your need
crossAxisCount: context.responsive(1, md: 2, xl: 4),
children: widge,
),
),
...
And finally, we have a card widget with stacked icons written in Flutter code.
Full code and demo can be found on Dartpad:
Thank you for reading!
Also published here.