Need mock data for your designs or tests? Get it instantly with no setup required!
Building a Dynamic Background Effect
How to create a randomized, animated icon background for any content section.
In this article, we’ll be building a subtle animation effect where background elements and icons are randomly positioned and move in the direction of a mouse cursor.
This particular example was inspired by the homepage of Codewars.
An effect like this can help add depth and interactivity to an otherwise static content section.
This article will be using React, but the overall concept can be applied to any frontend stack.
Getting Started
Start by creating a file named Container.js
. This component will output a hero section and contains pretty much everything we're building.
In this Container
component, we set up a state value, along with some refs and HTML markup.
import { useEffect, useRef, useState } from "react";
import cn from "classnames";
import { icons } from "./icons";
import "./styles.css";
export const Container = () => {
const [hasMounted, setHasMounted] = useState(false);
const heroRef = useRef();
const iconsRef = useRef([]);
const iconCount = icons?.length;
const { offsetWidth: width, offsetHeight: height } = heroRef?.current ?? {};
useEffect(() => {
setHasMounted(true);
}, []);
return (
<div className="hero" ref={heroRef}>
{hasMounted && (
<div className="icon-layer">
{icons.map(({ size, type, icon }, i) => {})}
</div>
)}
<div className="hero-copy">
<h1>
Grow your
<br /> <span>frontend skills</span>
</h1>
<h3>Learn advanced React, CSS, and JavaScript techniques.</h3>
<a href="#">Get Started</a>
</div>
</div>
);
};
The heroRef
tells us about the hero
section, including its dimensions so we know when the cursor is moving through it. The iconsRef
contains all of the icons and information about them.
The
hasMounted
state value we set above is being used to delay the rendering of the background elements until their positioning can be calculated. Without this, you’d see all of the the items stacked on top of each other on the very first render, then be positioned correctly.
We’ll revisit this Container
component in a minute.
Icons
Next add a new file named icons.js
.
The icons we're using are from Tabler Icons. You can also get them from the CodeSandbox demo.
Each item in the icons
array has a size
, type
, and may be empty if icon
is null
. If no icon
is passed, it'll just be an outline or filled-in element alongside the icons.
The import syntax below may look different depending on your project.
import { ReactComponent as ReactIcon } from "./icons/react.svg";
import { ReactComponent as AngularIcon } from "./icons/angular.svg";
import { ReactComponent as DenoIcon } from "./icons/deno.svg";
import { ReactComponent as JSIcon } from "./icons/javascript.svg";
import { ReactComponent as NPMIcon } from "./icons/npm.svg";
import { ReactComponent as TSIcon } from "./icons/typescript.svg";
import { ReactComponent as VueIcon } from "./icons/vue.svg";
import { ReactComponent as ReduxIcon } from "./icons/redux.svg";
export const icons = [
{ size: "lg", type: "fill", icon: <ReactIcon /> },
{ size: "sm", type: "outline", icon: null },
{
size: "lg",
type: "outline",
icon: <AngularIcon />,
},
{ size: "sm", type: "fill", icon: null },
{ size: "lg", type: "outline", icon: <DenoIcon /> },
{ size: "sm", type: "outline", icon: null },
{ size: "md", type: "fill", icon: <VueIcon /> },
{ size: "sm", type: "fill", icon: null },
{ size: "md", type: "outline", icon: null },
{ size: "sm", type: "outline", icon: null },
{ size: "md", type: "outline", icon: null },
{ size: "lg", type: "outline", icon: <JSIcon /> },
{ size: "md", type: "fill", icon: null },
{ size: "sm", type: "outline", icon: null },
{ size: "lg", type: "fill", icon: null },
{ size: "md", type: "fill", icon: null },
{ size: "sm", type: "fill", icon: <ReduxIcon /> },
{ size: "md", type: "fill", icon: null },
{ size: "sm", type: "fill", icon: null },
{ size: "lg", type: "fill", icon: <TSIcon /> },
{ size: "sm", type: "outline", icon: null },
{ size: "md", type: "fill", icon: <NPMIcon /> },
{ size: "md", type: "outline", icon: null },
];
Random Positioning
Now that the icons are defined, we need to render them and provide a randomized position when the component is first mounted.
Revisiting the Container
component we added previously, we’ll map
over each item.
Each one will be assigned a position based on its order in the icons
array. We divide all of the elements into rows and columns, ensuring they are laid out in a grid pattern and determine the width and height based on the number of cols
and rows
.
<div className="icon-layer">
{icons.map(({ size, type, icon }, i) => {
// Determine the rows and columns based on the amount of icons we have
const rows = Math.ceil(Math.sqrt(iconCount));
const cols = Math.ceil(iconCount / rows);
// Determines the size of each sell
const cellWidth = width / cols;
const cellHeight = height / rows;
})}
</div>
Next we'll determine the specific row
and col
that each item appears in using its index. This ensures that every icon gets its own “cell” in the grid.
{icons.map(({ size, type, icon }, i) => {
...
const row = Math.floor(i / cols);
const col = i % cols
})}
A slight offset within each cell (75px) is also added. Feel free to experiment with this value.
{icons.map(({ size, type, icon }, i) => {
...
const xOffset = Math.random() * (cellWidth - 75);
const yOffset = Math.random() * (cellHeight - 75);
const baseX = col * cellWidth;
const baseY = row * cellHeight;
const xPos = baseX + xOffset;
const yPos = baseY + yOffset;
})}
The xPos
and yPos
constants define the exact final positioning in each cell.
With everything calculated, return each element as a span
.
{icons.map(({ size, type, icon }, i) => {
...
return (
<span
key={i}
ref={refItem =>
(iconsRef.current[i] = {
refItem,
// Save initialPositions object and size on the ref itself for easy access later
initialPositions: {
xPos,
yPos,
},
size,
})
}
className={cn("icon", type, size, `pos-${i}`)}
style={{
transform: `translateX(${xPos}px) translateY(${yPos}px)`,
}}
>
{icon && icon}
</span>
);
})}
Notice how we’re storing an
initialPositions
object andsize
values onto each ref. Often you’ll see React refs used to get references to an HTML element, but they can be used to store other types of data too. Here we’re using refs for both because getting thetranslateX
andtranslateY
values from an element is not very straightforward. Since we already know what those values are to begin with, we may as well store them to reference later. We'll revisit this in a few minutes.
CSS
Now let’s add some CSS into a new file named styles.css
.
This section uses modern CSS features like nesting and variables. If you’re unfamiliar with either, please check out Dynamic Styling with CSS Variables and Nesting in CSS to learn more.
First we have some CSS variables. These are defined at the root level and set some commonly used values like colors and border radius.
:root {
--border-radius: 8px;
--black: #000;
--white: #fff;
--red: #ad1d1d;
--gray: #414141;
}
A .hero
class defines the layout and background of the hero section. We use a radial gradient to create a soft background effect. The hero content itself is centered using flexbox
.
.hero {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin: 0 auto;
padding: 150px 0;
position: relative;
height: 100vh;
overflow: hidden;
background: radial-gradient(
circle,
hsla(30, 100%, 8%, 1) 0%,
rgb(0, 0, 0) 100%
);
}
The .hero-copy
class styles the copy for both the heading and sub-heading.
.hero-copy {
text-align: center;
color: var(--white);
max-width: 750px;
z-index: 10;
h1 {
font-size: 76px;
margin: 0 0 30px;
font-weight: 300;
span {
background: linear-gradient(30deg, #ff8000 40%, var(--red) 70%);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
}
h3 {
font-size: 20px;
font-weight: 300;
}
a {
margin-top: 20px;
display: inline-block;
color: var(--white);
text-decoration: none;
font-size: 20px;
border-radius: 8px;
background: var(--red);
padding: 20px 30px;
}
}
To learn more about how to apply gradients and other examples of CSS text effects, check out a past article - CSS Text Effects - Five Minimal Examples.
The .icon-layer
class ensures that the icons sit above the background but behind the text. This layer covers the entire hero section, with icons positioned absolutely within it.
.icon-layer {
position: absolute;
height: 100%;
width: 100%;
}
Finally we have the .icon
class. This sets the positioning, color, and size for each element. We’ll also add an animation keyframe for a basic fade-in effect when the page first loads.
.icon {
position: absolute;
border-radius: 12px;
transition: transform 1s ease-out;
animation: iconFadeIn 7s ease forwards;
padding: 10px;
svg {
width: 100%;
height: 100%;
}
path {
color: var(--white);
}
&.outline {
border: 1px solid var(--gray);
}
&.fill {
background: var(--gray);
}
&.sm {
height: 40px;
width: 40px;
z-index: 1;
}
&.md {
height: 60px;
width: 60px;
z-index: 3;
}
&.lg {
height: 80px;
width: 80px;
z-index: 5;
}
}
@keyframes iconFadeIn {
from {
opacity: 0;
}
to {
opacity: 0.6;
}
}
Moving the Icons
So far we have background elements and icons randomly positioned on page load. Let’s add some movement!
First, let’s revisit the useEffect
we added previously to the Container
component.
We’ll use the heroRef
that we set up and add a mousemove
event to the hero section. This gives us a way to detect when the mouse cursor is moving through that section specifically. We'll remove the event if the component unmounts.
export const Container = () => {
...
useEffect(() => {
setHasMounted(true);
const refVal = heroRef?.current;
refVal.addEventListener("mousemove", onHover);
return () => refVal.removeEventListener("mousemove", onHover);
}, []);
return ( ... );
}
Then we have the onHover
function itself.
This function gets the current X and Y position of the cursor and determines how to position each icon accordingly. It applies these hoverX
and hoverY
values onto the original position (xPos/yPos
) so that the element doesn’t move too far from where it was originally rendered.
export const Container = () => {
...
const onHover = ({ clientX, clientY }) => {
iconsRef.current.forEach(item => {
const { xPos, yPos } = item?.initialPositions ?? {};
const size = item.size;
const { hoverX, hoverY } = setThresholds(size, clientX, clientY);
item.refItem.style.transform = `translate(${xPos + hoverX}px, ${yPos + hoverY}px)`;
});
};
...
return ( ... );
}
This is why we stored the
initialPositions
object directly on the ref. We needed an easy way to get the original X and Y position for each item so that we could modify it with the hover position. To get these values cleanly from theitem
ref, you’d otherwise need to use regex or string manipulation because it would look exactly how we set it initially, for example,translateX(100px) translateY(150px)
.
Finally we’ll add a function named setThresholds
that takes the icon size and cursor position and determines how far to actually move each element, depending on its size.
const setThresholds = (size, x, y) => {
if (size === "sm") {
return {
hoverX: x * 0.03,
hoverY: y * 0.03,
};
}
if (size === "md") {
return {
hoverX: x * 0.06,
hoverY: y * 0.06,
};
}
if (size === "lg") {
return {
hoverX: x * 0.1,
hoverY: y * 0.1,
};
}
};
These thresholds are what provide a sort of “parallax” effect, where the larger elements move more than the smaller ones. Feel free to experiment with these values.
Limitations
We’ve finished the background animation effect, but there are a few final things to consider.
Responsiveness
In this example, the icon positions are only calculated on page load. You would need to recalculate those for a fully dynamic effect if the screen were to be resized after the pages loads.
Accessibility
In a real-world situation, you will have users who would rather not see animations like this. Consider reducing or removing the animations for them. Be sure to check out Reducing Motion in Animations for more information.
Summary
In this article, we created a subtle, interactive hero section that responds to mouse movements. This effect is perfect for adding a dynamic touch to your website’s landing page or any prominent section.
Feel free to experiment with different icon layouts, movement speeds, and styles to make the effect your own!