Recently a number of sites, including Bioware’s promotional page for Mass Effect Andromeda and Active Theory’s work for Under Armour have featured a parallax effect that ties mouse movement to the motion of a page element in the opposite direction. I was interested in recreating the effect using vanilla JavaScript and modern CSS, an example of which you can see above.
Get In The Ring
The markup consists of a <div>
container, an image, a heading, and body text (extracted from the short story “A Piece of Steak” by Jack London) surrounded by another <div>
:
<div id="boxercontainer">
<img src="boxer.png" alt>
<div>
<h1>The Boxer</h1>
<p>Well, a man had only so many fights in him, to begin with…
</div>
</div>
The boxer was extracted from his background and turned into an alpha-masked PNG, reduced in file size by converting it to a 16-bit image.
The outer #boxercontainer
element is styled: note that the background-image
is made slightly larger than its container and positioned from its center, facts that will be important later:
#boxercontainer {
width: 80%;
max-width: 900px;
margin: 0 auto;
background-image: url(concrete-background.jpg);
position: relative;
padding-bottom: 45%;
background-size: 120% 120%;
background-position: 50% 50%;
overflow: hidden;
min-height: 650px;
}
The inner div
is given a transparent, slightly dark background color to increase the contrast of the text inside it:
#boxercontainer div {
position: absolute;
width: 60%;
left: 20px;
top: 20px;
border: 1px solid #fff;
padding: 2rem;
background: rgba(0,0,0,0.2);
}
The image is also positioned absolutely (taking advantage of the absolute-position-inside-relative trick), dropped down so that the lower portion of the boxer’s legs are hidden by the overflow: hidden
on the parent element. It’s also provided with a drop-shadow
filter, making a far smaller image file size than baking the drop-shadow into the PNG; as we’ll see in a moment, this also createsfar more dynamic options for the image.
#boxercontainer img {
position: absolute;
bottom: -35px;
right: 50px;
width: 40%;
filter: drop-shadow(-200px 200px 50px #000);
padding: 1rem;
z-index: 2;
}
The z-index
value of 2
places the boxer visually above the div
containing the text, but it also means that the shadow falls over the words. To correct that, I modified the appearance of the paragraph text:
#boxercontainer p {
position: relative;
z-index: 3;
}
With a higher z-index
, the shadow now falls under the text, but over the border
on the div.
Getting Centered
The core idea of the parallax effect is finding the center of the affected element. The cursor’s distance from this center will displace the element by the same distance in the opposite direction:
To make this happen, the script starts by identifying the elements (using the fact that an id
automatically turns into a reference in JavaScript, and using a querySelector
for the boxer:
const boxer = boxercontainer.querySelector("img"),
maxMove = boxercontainer.offsetWidth / 30,
boxerCenterX = boxer.offsetLeft + (boxer.offsetWidth / 2),
boxerCenterY = boxer.offsetTop + (boxer.offsetHeight / 2);
maxMove
is the maximum distance we want the boxer to move, since we don’t usually want the image to move with complete freedom in response to the mouse being anywhere on the page.
We’ll also need to determine the position of the mouse inside boxercontainer
, for which I’ll use a function:
function getMousePos(xRef, yRef) {
let panelRect = boxercontainer.getBoundingClientRect();
return {
x: Math.floor(xRef - panelRect.left) /
(panelRect.right - panelRect.left)*boxercontainer.offsetWidth,
y: Math.floor(yRef - panelRect.top) /
(panelRect.bottom - panelRect.top) * boxercontainer.offsetHeight
};
}
Float Like a Butterfly
Effects like this usually respond to mouse movement in the page:
document.body.addEventListener("mousemove", function(e) {
let mousePos = getMousePos(e.clientX, e.clientY),
distX = mousePos.x - boxerCenterX,
distY = mousePos.y - boxerCenterY;
if (Math.abs(distX) < 500 && distY < 200) {
boxer.style.transform =
"translate("+(-1 * distX) / 12 + "px," + (-1 * distY) / 12 + "px)";
}
})
distX
is the horizontal distance between the current mouse position and the initial center of the boxer image; distY
is the vertical distance. If the vertical difference (positive or negative) is less than 500px, and the horizontal distance less than 200px, then we move the boxer image using a CSS transform:
- both distances are multiplied by
-1
(turning a positive distance into a negative, and a negative into a positive) - the result is then divided by 12, to reduce the ratio of movement between the mouse and the image (a 1:1 relationship between mouse position and image position would make movement far too confusing).
When we move our head position, it’s not only elements in the foreground that change position; objects in the background also shift, in the opposite direction. Think of an object placed a few feet in front of a wall: moving to the right will change your perspective on both.
To recreate this effect, I also move the background-image
of the concrete wall. The addEventListener
callback has the following added at the end of the script:
boxercontainer.style.backgroundPosition =
`calc(50% + ${distX / 50}px) calc(50% + ${distY / 50}px)`;
To make the background move, I’m using calc
to produce an offset from the background image’s default center position. (I’m also using template literals to make concatenation easier).
Counted Out
As a general rule, parallax effects work well on larger desktop and laptop screens, but not at all on smaller viewports. Because the effect is created with JavaScript, we must also detect the viewport width with JS, via matchMedia
:
let fluidboxer = window.matchMedia("(min-width: 726px)");
The if
condition for moving the image and background changes to:
if (Math.abs(distX) < 500 && distY < 200 && fluidboxer.matches) { … }
There are more changes in CSS @media
queries to change the design at smaller viewport sizes; see the CodePen demo for more details.
Punch-Drunk
Parallax effects can adversely affect users with vestibular disorders (sensitivity to visual motion and change) to the point of nausea and seizure; at the minimum, displaying a warning or some other indication that the page contains motion effects is a good idea. Browser vendors are currently working on a “reduce motion” user setting that will be detectable using an @media
query in the near future; in response, features like parallax effects could be automatically turned off on the page for affected users.
Conclusion
Using CSS transforms with JavaScript mouse events can be a cheap and relatively easy way to achieve parallax effects, with a little math. In future articles, I’ll explore these kinds of effects further.
Photograph by Terry George, licensed under Creative Commons
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/qqqQNe