Looking beyond :hover and :active
What if you wanted to make a user aware of a piece of content long before their mouse directly hovers over it’s element? Perhaps it was a piece of content that a user would have never otherwise hovered over (thus rendering all that css you styled on the :hover of your class forever invisible to your user). What is a designer/developer to do?
Traditional methods depend solely on pseudo classes like :hover and :active to provide basic interactivity when a user’s mouse is within the boundaries of it’s element. But these methods are limited because they require user intent to align with your goals long before they discover the styles hidden behind :hover.
Well, that’s where Reactive Listeners come into play. Reactive Listeners allow us to affect change over an element from a distance that otherwise would not be possible. In this example, we detect the proximity of the mouse pointer to an element which reacts more vibrantly the closer to user’s mouse comes to it. Here we use this effect to draw attention to some additional information contained in a hidden div, but many more practical uses abound, like drawing attention to a share button, or CTA.
This neat little trick uses the psychology of change detection; the human brain is wired to notice even the most minute micro changes in an otherwise static environment. In the past, designers used animations tied to timers, scrollheight, page loads etc. to take advantage of this mental hack. All can be effective methods, but require a specific set of criteria to be met that may be out of sync with the user’s focus.
Enough lecturing, let’s find out how it works:
How It Works
Start by adding the reactive listener javascript to your page
<script src="js/reactive-listener.js"></script>
Next, you'll need to identify the elements you want to animate, what property you want to animate, and what behavior they're going to be reacting to, for example x distance from mouse, y distance from mouse, or 2d distance from mouse*. Once you have these, you'll initialize the reactive listener, and let it handle things from there. In the example above, we're animating opacity on a '.glow' element based on 2d distance once the mouse is within 450 pixels. For simplicity, lets show what this looks like with a single element with id 'glow' and a single animation:
var glow = document.getElementById('glow'); ReactiveListener.add(glow, { 'Pointer2d': [ property: 'opacity', range: [1, 0], unit: '', maxDist: 450, forceMax: true, directional: false, start: 0 ] }) ReactiveListener.start();
Breaking down the pieces, what we've done here is first find the element, then add a listener that will react to the 2d distance ('Pointer2d') with a particular configuration. What are those different configuration settings doing?
- property: This is the property being animated, in this case opacity.
- range: What range are we animating between? For opacity, we want to go between 1 (visible) and 0 (invisible)
- unit: This is the unit being applied to the range... for opacity there is no unit, but if we were doing a translate or something similar, we might need a 'px' or a 'rem' as unit.
- maxDist: What is the maximum distance where we want to have any sort of animation... in this case we don't want to do anything until we get below 450.
- forceMax: If we're outside the max, should we do nothing? Or force the value to the max? In this case we want to force to the max (invisible), but if we were chaining another animation we might want to do nothing and let the later animation take control.
- directional: Does it matter if we're positive or negative relative to the object? For 2d distance, we'll always be positive, but if we were listining to x or y only the concepts of positive and negative matter.
- start: What value should the animation start at?
The extra cool thing comes when we start to chain behaviors. A single element can have any number of reactive animations chained into the listener. Each animation is calculated in order, and then applied in bulk, so if there are multiple animations that apply to the same property (say you have two overlapping ranges), the last one in the array applies.
Under the Covers
What's really going on here is that we are consolidating all of these distance based animations into a very tight calculation loop that is triggered based on mouse movements. When the mouse moves, it triggers a set of 'requestAnimationFrame' events that will animate the various elements into the correct positions.
Have a behavior you want to animate that doesn't easily map into a single property? Or want to influence a different element on the page based on the distance from a single one? Instead of passing a configuration hash, you can hook into the same tight loop by passing an object with a callback property:
var glow = document.getElementById('glow'); ReactiveListener.add(glow, { callback: function(opts) { var dist = opts.dist['Pointer2d']; if(dist === 0) { return false; } else { if(dist < 50) { var height = ((50 - dist) / 50) * 150; $('#hidden-area').css({height: height, opacity: (50 - dist)/100}); } } } });
The callback should be lightweight and fast, as it will be called within every frame of a 'requestAnimationFrame' and be passed an options object containing an three components: a representation of the item being triggered on called 'item', calculations of the x, y, and 2d distance in an object called 'dist', and an object representing the mouse position called 'mouse'.
Try It Yourself
Want to try it out? You can install the reactive listener via NPM npm install reactive-listener
or directly from Github
You can also play with it immediately using CodePen. Here is the example from above:
See the Pen Reactive Listener Playground Example by Kevin Ball (@kball) on CodePen.
*Beyond Mouse Position
The reactive listener is currently a prototype where the only real thing it knows how to watch for is mouse position, but the core concept is extensible, and we can imagine reacting to things like scroll, rotation, GPS, and a variety of other data sources. Let us know what you're interested in and we'll see what we can make happen!