Flutter Web: Mouse Hover Parallax Effect

Mariano Zorrilla
Flutter Community
Published in
7 min readMar 31, 2020

There’s always something so interesting about animation and parallax is one of those who always catches my eyes 😍

Parallax is nothing new, it has been around for decades and was one of those first effects who added extra layers of depth into video games, movies and simple 3D-like samples.

NES Parallax Effect

The world of mobile development is not an exception for this rule! From collapsing toolbars, animated background, scrollable pages, everything looks WAY BETTER with some good parallax on it.

iOS parallax background

It looks and sounds tricky but, in reality, is pretty simple… things that are “far away” will move slower than the ones closer to us. I’m sure you notice the same while taking a trip, watching through the windows of the car, and checking how “slow” those far away cows or mountains move but those fences close to our car go really “fast”.

Android collapsing toolbar parallax effect

But enough introduction! It’s CODING TIME 🤓💻

-First thing first… where to start? Why there’s no guides?! What I need to change?! WHY?! WHY?!!!

-Relax, I said!!!

average drama in Latin America telenovelas

There’s one thing we know… when click pan or hover with our mouse, we need to update several values, the first ones in mind are “rotation X and Y” and Flutter always has a Widget ready for that:

Transform 🥰

This baby boy has sooooooooo (x100) many methods available to modify Widgets, like, so many! You want to scale, skew, translate, rotate, etc you name it, it has it all.

One thing you’d notice while using “Transform” is the class Matrix4, and no, Neo has nothing to do with this…

and not even the incredible coincidence of Matrix 4 coming in 2021 👀

Matrix4 is nothing more than a vector 4D with x, y , z, scale factor and several other goodies. You can even read more about it over here:

For our cool demo, we’re going to use “rotateX”, “rotateY”, “setEntry” (this one is super interesting as it will create our 3D effect) and “translate”.

Ok, first thing first, we need to do some measures:

final size = MediaQuery.of(context).size;

that way we can check out width and height between several calculation needed for this trick.

We need as well some variables to achieve the rotate + parallax effect:

double localX = 0;
double localY = 0;
bool defaultPosition = true;

those will be super helpful while panning and hovering our Widget. We also need the percentage of where your mouse/finger is related to our Widget:

double percentageX = (localX / (size.width - 40)) * 100;
double percentageY = (localY / 230) * 100;

I this case, my Widget has a size of “size.width — 40” for its width and “230” for the height.

If we opt to pan our Widget, we need to use the good old GestureDetector:

GestureDetector(
onPanCancel: () => setState(() => defaultPosition = true),
onPanDown: (_) => setState(() => defaultPosition = false),
onPanEnd: (_) => setState(() {
localY = 115;
localX = (size.width - 40) / 2;
defaultPosition = true;
}),
onPanUpdate: (details) {
if (mounted) setState(() => defaultPosition = false);
if (details.localPosition.dx > 0 &&
details.localPosition.dy < 230) {
if (details.localPosition.dx < size.width - 40 &&
details.localPosition.dy > 0) {
localX = details.localPosition.dx;
localY = details.localPosition.dy;
}
}
}

and if you take a closer look, you’d notice that I’m taking half of width and height to return the view to its default position, while onPanUpdate gets all the local position for that Widget between the ranges of itself.

One we get that, we’ll apply our Transformation, and the following code achieves that goal:

Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(defaultPosition ? 0 : (0.3 * (percentageY / 50) + -0.3))
..rotateY(defaultPosition ? 0 : (-0.3 * (percentageX / 50) + 0.3)),
alignment: FractionalOffset.center,
child: Container(), // back layer
)

like I said before, setEntry(3, 2, 0.001) plays a huge role here as this one will create the desire 3D effect for the back layer and our rotation will look soft and smooth. Oh, very important, we need a center pivot for this to happen, if not, position 0,0 will be the selected one:

alignment: FractionalOffset.center fixes the “issue”.

What, we need to create our parallax effect for the front layer, this one will move “more” as it should be “closer” to us… like the road trip I talked before.

Moving our X and Y positions, just a little bit, will add some extra movement from our back layer.

Remember, this from layer should be inside a Stack Widget to have freedom of movement and be on top (Z position):

Matrix4.identity()
..translate(
defaultPosition
? 0.0
: (15 * (percentageX / 50) + -15),
defaultPosition
? 0.0
: (15 * (percentageY / 50) + -15),
0.0),
alignment: FractionalOffset.center,
child: Container(), // front layer
}

Pufffff… 😓 after all this math work we need to relax a bit, but enjoy the cool thing we just created!

SFXP Meetup — 2 Layers Parallax

But this Medium post was about using a mouse… not a finger, what about Hovering?

Cool thing about hover effect is that you don’t even need to touch the Widget, you need to move your mouse cursor on top of it and get some information about it:

MouseRegion 🐁 🖱

This cool Widget allows us to know when our mouse cursor: Enter, Exit and Hover our Widget.

MouseRegion(
onEnter: (_) => setState(() => defaultPosition = false),
onExit: (_) => setState(() {
localY = (size.height) / 2;
localX = (size.width * 0.45) / 2;
defaultPosition = true;
}),
onHover: (details) {
if (mounted) setState(() => defaultPosition = false);
if (details.localPosition.dx > 0 && details.localPosition.dy > 0) {
if (details.localPosition.dx < (size.width * 0.45) * 1.5 && details.localPosition.dy > 0) {
localX = details.localPosition.dx;
localY = details.localPosition.dy;
}
}
},

Our logic it’s pretty much the same as GestureDetector but we have one less method to be called.

Following our logic from before:

Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(defaultPosition ? 0 : (1.2 * (percentageY / 50) + -1.2))
..rotateY(defaultPosition ? 0 : (-0.3 * (percentageX / 50) + 0.3)),
alignment: FractionalOffset.center,
child: Container(), // back layer
)

and for our front layer:

Transform(
transform: Matrix4.identity()
..translate(defaultPosition ? 0.0 : (70 * (percentageX / 50) + -70),
defaultPosition ? 0.0 : (80 * (percentageY / 50) + -80), 0.0),
alignment: FractionalOffset.center,
child: Container(), // front layer

What’s the result from all this nerd data?

some fancy cool 2 layers parallax effect on Flutter Web hovering the mouse cursor.

But, you know what’s actually REALLY COOL?! This works on DartPad as well!

Flutter really has the power to run everywhere 💻📱

BUT, WAIT! There’s more… since Dart 2.6+ we can add some extension goodies to our project.

add an ID to your <body> index.html:

<body id="app-container">

and make your mouse cursor change to a pointing finger while hovering:

import 'dart:html' as html;

extension HoverExtensions on Widget {
static final appContainer = html.window.document.getElementById('app-container');

Widget get showCursorOnHover {
return MouseRegion(
child: this, // the widget we're using the extension on
onHover: (event) => appContainer.style.cursor = 'pointer',
onExit: (event) => appContainer.style.cursor = 'default',
);
}

Widget get moveUpOnHover {
return TranslateOnHover(
child: this,
);
}
}

then, all you need to do, is add the getter “showCursorOnHover” to any Widget you like at the end of it:

Container().showCursorOnHover;

and your ↖️ will become a “call to action” 👆 like this!

Make sure your pubspec.yaml uses Dart 2.6+:

environment:
sdk: ">=2.6.0 <3.0.0"

--

--

Mariano Zorrilla
Flutter Community

GDE Flutter — Tech Lead — Engineer Manager | Flutter | Android @ Venmo