Why build a generator
Front-end generators are a low-stakes way to learn a new technique and sharpen your skills, whatever your experience level. Familiar examples include Sarah Drasner's Hero Generator and CSS Grid Generator, the Glassmorphism CSS Generator from Themesburg, and the Text Shadows tool from Components AI, which ships many more generators besides.
The pattern generalizes: any cool front-end effect you run into is a candidate for an interactive generator. Here we'll walk through one for an Animated Background Gradient Generator.
Scaffolding
This is your project, so pick whatever stack you like. This build starts as a basic Create Next App project.
npx create-next-app animated-gradient-background-generator
The generated files are enough to begin. pages/index.js becomes the shell for the app.
import Head from "next/head"
import Image from "next/image"
export default function Home() {
return (
<>
<Head>
<title>Animated CSS Gradient Background Generator</title>
<meta name="description" content="A tool for creating animated background gradients in pure CSS." />
<link rel="icon" href="https://css-tricks.com/favicon.ico" />
</Head>
<main>
<h1>
Animated CSS Gradient Background Generator
</h1>
</main>
</>
)
}
The animation foundation
The first search result for an animated CSS gradient background is this Pen by Manuel Pinto, and its CSS is a solid base for the generated animation.
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
Describing a gradient in React
Three settings make sense as user-configurable options: an array of gradient colors, the gradient's angle, and its animation speed.
Those settings live in a higher-order provider component, context/SettingsContext.js, along with some defaults, so every part of the small app can read them.
import React, { useState, createContext } from "react"
const SettingsContext = createContext({ colorSelection: [] })
const SettingsProvider = ({ children }) => {
const [colorSelection, setColorSelection] = useState([
"deepskyblue",
"darkviolet",
"blue",
])
const [angle, setAngle] = useState(300)
const [speed, setSpeed] = useState(5)
return (
<SettingsContext.Provider
value={{
colorSelection,
setColorSelection,
angle,
setAngle,
speed,
setSpeed,
}}
>
{children}
</SettingsContext.Provider>
)
}
export { SettingsContext, SettingsProvider }
Three kinds of components follow: controls to adjust settings, a visual display of the animated gradient, and an output component for the CSS code.
Color controls
A Controls component holds the inputs that adjust the settings.
import Colors from "./Colors"
const Controls = (props) => (
<>
<Colors />
</>
)
export default Controls
Add SettingsProvider and Controls to pages/index.js.
import Head from "next/head"
import Image from "next/image"
import { SettingsProvider } from "../context/SettingsContext"
import Controls from "../components/Controls"
import Output from "../components/Output"
export default function Home() {
return (
<>
<Head>
...
</Head>
<SettingsProvider>
<main style={{ textAlign: "center", padding: "64px" }}>
<h1>Animated CSS Gradient Background Generator</h1>
<Controls />
<Output />
</main>
</SettingsProvider>
</>
)
}
SettingsProvider starts with the three colors from the CodePen example as its defaults. A new Colors component shows that the color settings are reachable through SettingsContext.
import React, { useContext } from "react"
import { SettingsContext } from "../context/SettingsContext"
const Colors = () => {
const { colorSelection } = useContext(SettingsContext)
return (
<>
{colorSelection.map((color) => (
<div>{color}</div>
))}
</>
)
}
export default Colors
The Colors component renders one swatch per color, each with a small delete button wired through SettingsContext.
import React, { useContext } from "react"
import { SettingsContext } from "../context/SettingsContext"
const Colors = () => {
const { colorSelection, setColorSelection } = useContext(SettingsContext)
const onDelete = (deleteColor) => {
setColorSelection(colorSelection.filter((color) => color !== deleteColor))
}
return (
<div>
{colorSelection.map((color) => (
<div
key={color}
style={{
background: color,
display: "inline-block",
padding: "32px",
margin: "16px",
position: "relative",
borderRadius: "4px",
}}
>
<button
onClick={() => onDelete(color)}
style={{
background: "crimson",
color: "white",
display: "inline-block",
borderRadius: "50%",
position: "absolute",
top: "-8px",
right: "-8px",
border: "none",
fontSize: "18px",
lineHeight: 1,
width: "24px",
height: "24px",
cursor: "pointer",
boxShadow: "0 0 1px #000",
}}
>
×
</button>
</div>
))}
</div>
)
}
export default Colors
Inline styles carry the CSS at this point — it's a playground, so that's fine.
Adding colors to the gradient requires an AddColor component: a button that opens a color picker. For the picker, install react-color and use its ChromePicker option.
npm install react-color
Here too SettingsContext handles updating the gradient color selection.
import React, { useState, useContext } from "react"
import { ChromePicker } from "react-color"
import { SettingsContext } from "../context/SettingsContext"
const AddColor = () => {
const [color, setColor] = useState("white")
const { colorSelection, setColorSelection } = useContext(SettingsContext)
return (
<>
<div style={{ display: "inline-block", paddingBottom: "32px" }}>
<ChromePicker
header="Pick Colors"
color={color}
onChange={(newColor) => {
setColor(newColor.hex)
}}
/>
</div>
<div>
<button
onClick={() => {
setColorSelection([...colorSelection, color])
}}
style={{
background: "royalblue",
color: "white",
padding: "12px 16px",
borderRadius: "8px",
border: "none",
fontSize: "16px",
cursor: "pointer",
lineHeight: 1,
}}
>
+ Add Color
</button>
</div>
</>
)
}
export default AddColor
Angle and speed
Range inputs handle the remaining two settings. Below is AngleRange; SpeedRange is very similar.
import React, { useContext } from "react"
import { SettingsContext } from "../context/SettingsContext"
const AngleRange = () => {
const { angle, setAngle } = useContext(SettingsContext)
return (
<div style={{ padding: "32px 0", fontSize: "18px" }}>
<label
style={{
display: "inline-block",
fontWeight: "bold",
width: "100px",
textAlign: "right",
}}
htmlFor="angle"
>
Angle
</label>
<input
type="range"
id="angle"
name="angle"
min="-180"
max="180"
value={angle}
onChange={(e) => {
setAngle(e.target.value)
}}
style={{
margin: "0 16px",
width: "180px",
position: "relative",
top: "2px",
}}
/>
<span
style={{
fontSize: "14px",
padding: "0 8px",
position: "relative",
top: "-2px",
width: "120px",
display: "inline-block",
}}
>
{angle} degrees
</span>
</div>
)
}
export default AngleRange
Rendering the background
The AnimatedBackground wrapper component applies the effect to the entire page background.
import React, { useContext } from "react"
import { SettingsContext } from "../context/SettingsContext"
const AnimatedBackground = ({ children }) => {
const { colorSelection, speed, angle } = useContext(SettingsContext)
const background =
"linear-gradient(" + angle + "deg, " + colorSelection.toString() + ")"
const backgroundSize =
colorSelection.length * 60 + "%" + " " + colorSelection.length * 60 + "%"
const animation =
"gradient-animation " +
colorSelection.length * Math.abs(speed - 11) +
"s ease infinite"
return (
<div style={{ background, "background-size": backgroundSize, animation, color: "white" }}>
{children}
</div>
)
}
export default AnimatedBackground
The wrapper calls the gradient animation gradient-animation, which must be added to styles/globals.css to trigger the actual animation.
@keyframes gradient-animation {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
Output and extras
A code output component lets visitors copy and paste the generated CSS into their own projects.
import React, { useContext, useState } from "react"
import { SettingsContext } from "../context/SettingsContext"
const Output = () => {
const [copied, setCopied] = useState(false)
const { colorSelection, speed, angle } = useContext(SettingsContext)
const background =
"linear-gradient(" + angle + "deg," + colorSelection.toString() + ")"
const backgroundSize =
colorSelection.length * 60 + "%" + " " + colorSelection.length * 60 + "%"
const animation =
"gradient-animation " +
colorSelection.length * Math.abs(speed - 11) +
"s ease infinite"
const code = `.gradient-background {
background: ${background};
background-size: ${backgroundSize};
animation: ${animation};
}
@keyframes gradient-animation {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}`
return (
<div
style={{ position: "relative", maxWidth: "640px", margin: "64px auto" }}
>
<pre
style={{
background: "#fff",
color: "#222",
padding: "32px",
width: "100%",
borderRadius: "4px",
textAlign: "left",
whiteSpace: "pre",
boxShadow: "0 2px 8px rgba(0,0,0,.33)",
overflowX: "scroll",
}}
>
<code>{code}</code>
<button
style={{
position: "absolute",
top: "8px",
right: "8px",
background: "royalblue",
color: "white",
padding: "8px 12px",
borderRadius: "8px",
border: "none",
fontSize: "16px",
cursor: "pointer",
lineHeight: 1,
}}
onClick={() => {
setCopied(true)
navigator.clipboard.writeText(code)
}}
>
{copied ? "copied" : "copy"}
</button>
</pre>
</div>
)
}
export default Output
A button that sets random values is a useful addition: it gives people a fast way to experiment with results, and it's a chance to look up things like generating random hex colors.
import React, { useContext } from "react"
import { SettingsContext } from "../context/SettingsContext"
const Random = () => {
const { setColorSelection, setAngle, setSpeed } = useContext(SettingsContext)
const goRandom = () => {
const numColors = 3 + Math.round(Math.random() * 3)
const colors = [...Array(numColors)].map(() => {
return "#" + Math.floor(Math.random() * 16777215).toString(16)
})
setColorSelection(colors)
setAngle(Math.floor(Math.random() * 361))
setSpeed(Math.floor(Math.random() * 10) + 1)
}
return (
<div style={{ padding: "48px 0 16px" }}>
<button
onClick={goRandom}
style={{
fontSize: "24px",
fontWeight: 200,
background: "rgba(255,255,255,.9)",
color: "blue",
padding: "24px 48px",
borderRadius: "8px",
cursor: "pointer",
boxShadow: "0 0 4px #000",
border: "none",
}}
>
RANDOM
</button>
</div>
)
}
export default Random
Before release, update package.json with your project information, link to your personal site and the project repository while crediting anything you borrowed, and replace the default README.md that Create Next App generated.
The code lives on GitHub, and the demo is hosted on Netlify.



