noise grid
February 8, 2021

Create a Generative Landing Page & WebGL Powered Background

Recently I took a trip to the faraway land of dribbble and saw something magical. There were fuzzy orbs and beautiful, glass-like interfaces floating around everywhere. Serene!

This got me thinking. Wouldn’t it be cool to create a generative landing page in this style?

The end result

First of all, here’s a kind of visual TL;DR.

You can check out a full-page example here, too.

The color palette is random within constraints. The colorful orbs move with a mind of their own. These elements of randomness are what make our landing page generative.

If generative art/design is new to you, here is an excellent primer from Ali Spittel & James Reichard.

Like what you see? Let’s build!


To get the most out of this tutorial you will need to be comfortable writing HTML, CSS, and JavaScript.

If you have read “WebGL” and fallen into a state of shader-induced panic, don’t worry. We will be using PixiJS to abstract away the scary stuff. This tutorial will serve as a nice introduction to Pixi if you haven’t used it before, too.

Creating the background animation

The first thing we are going to build is the orbs. To create them, we are going to need some libraries/packages. Let’s get the boring stuff out of the way first and add them to the project.

Package overview

Here’s a quick summary of the libraries/packages we will be using.

  • PixiJS - A powerful graphics library built on WebGL, we will use it to render our orbs.
  • KawaseBlurFilter - A PixiJS filter plugin for ultra smooth blurs.
  • SimplexNoise - Used to generate a stream of self-similar random numbers. More on this shortly.
  • hsl-to-hex - A tiny JS utility for converting HSL colors to HEX.
  • debounce - A  JavaScript debounce function.

Package installation

If you are following along on CodePen, add the following imports to your JavaScript file and you are good to go:

import * as PIXI from '';
import { KawaseBlurFilter } from '';
import SimplexNoise from '';
import hsl from '';
import debounce from '';

If you are hanging out in your own environment, you can install the required packages with:

npm i pixi.js @pixi/filter-kawase-blur simplex-noise hsl-to-hex debounce

You can then import them like so:

import * as PIXI from 'pixi.js';
import { KawaseBlurFilter } from '@pixi/filter-kawase-blur';
import SimplexNoise from 'simplex-noise';
import hsl from 'hsl-to-hex';
import debounce from 'debounce';

Note: Outside of CodePen you will need a build tool such as Webpack or Parcel to handle these imports.

A blank (Pixi) canvas

Awesome, we now have everything we need to get started. Let’s kick things off by adding a <canvas> element to our HTML:

<canvas class="orb-canvas"></canvas>

Next, we can create a new Pixi instance with the canvas element as it’s “view” (where Pixi will render). We will call our instance app:

// Create PixiJS app
const app = new PIXI.Application({
// render to <canvas class="orb-canvas"></canvas>
view: document.querySelector('.orb-canvas'),
// auto adjust size to fit the current window
resizeTo: window,
// transparent background, we will be creating a gradient background later using CSS
transparent: true,

If you inspect the DOM and resize the browser, you should see the canvas element resize to fit the window. Magic!

Some helpful utilities

Before going any further, we should add some utility functions to our JavaScript.

// return a random number within a range
function random(min, max) {
return Math.random() * (max - min) + min;

// map a number from 1 range to another
function map(n, start1, end1, start2, end2) {
return ((n - start1) / (end1 - start1)) * (end2 - start2) + start2;

If you have followed any of my tutorials before, you might be familiar with these already. I’m a little obsessed…

random will return a random number within a limited range. For example, “Give me a random number between 5 and 10”.

map takes a number from one range and maps it to another. For example, if a number (0.5) usually exists in a range between 0 - 1 and we map it to a range of 0 - 100, the number becomes 50.

I encourage experimenting with these two utilities a little if they are new to you. They will be useful companions in your generative journey! Pasting them into the console and experimenting with the output is a great place to start.

Creating the Orb class

Now, we should have everything we need to create our orb animation. To start, let’s create an Orb class:

// Orb class
class Orb {
// Pixi takes hex colors as hexidecimal literals (0x rather than a string with '#')
constructor(fill = 0x000000) {
// bounds = the area an orb is "allowed" to move within
this.bounds = this.setBounds();
// initialise the orb's { x, y } values to a random point within it's bounds
this.x = random(this.bounds['x'].min, this.bounds['x'].max);
this.y = random(this.bounds['y'].min, this.bounds['y'].max);

// how large the orb is vs it's original radius (this will modulate over time)
this.scale = 1;

// what color is the orb?
this.fill = fill;

// the original radius of the orb, set relative to window height
this.radius = random(window.innerHeight / 6, window.innerHeight / 3);

// starting points in "time" for the noise/self similar random values
this.xOff = random(0, 1000);
this.yOff = random(0, 1000);
// how quickly the noise/self similar random values step through time = 0.002;

// PIXI.Graphics is used to draw 2d primitives (in this case a circle) to the canvas = new PIXI.Graphics(); = 0.825;

// 250ms after the last window resize event, recalculate orb positions.
debounce(() => {
this.bounds = this.setBounds();
}, 250)

Our Orb is a simple circle that exists in a 2d space.

It has an x and a y value, a radius, a fill color, a scale value (how large it is vs its original radius) and a set of bounds. Its bounds define the area it can move around in, like a set of virtual walls. This will stop the orbs from getting too close to our text.

You may notice the use of a non-existent setBounds function in the snippet above. This function will define the virtual constraints our orbs exist within.  Let’s add it to the Orb class:

setBounds() {
// how far from the { x, y } origin can each orb move
const maxDist =
window.innerWidth < 1000 ? window.innerWidth / 3 : window.innerWidth / 5;
// the { x, y } origin for each orb (the bottom right of the screen)
const originX = window.innerWidth / 1.25;
const originY =
window.innerWidth < 1000
? window.innerHeight
: window.innerHeight / 1.375;

// allow each orb to move x distance away from it's { x, y }origin
return {
x: {
min: originX - maxDist,
max: originX + maxDist
y: {
min: originY - maxDist,
max: originY + maxDist

OK, great. This is coming together! Next up, we should add an update and a render function to our Orb class. Both of these functions will run on each animation frame. More on this in a moment.

The update function will define how the orb’s position and size should change over time. The render function will define how the orb should display itself on-screen.

First, here is the update function:

update() {
// self similar "psuedo-random" or noise values at a given point in "time"
const xNoise = simplex.noise2D(this.xOff, this.xOff);
const yNoise = simplex.noise2D(this.yOff, this.yOff);
const scaleNoise = simplex.noise2D(this.xOff, this.yOff);

// map the xNoise/yNoise values (between -1 and 1) to a point within the orb's bounds
this.x = map(xNoise, -1, 1, this.bounds["x"].min, this.bounds["x"].max);
this.y = map(yNoise, -1, 1, this.bounds["y"].min, this.bounds["y"].max);
// map scaleNoise (between -1 and 1) to a scale value somewhere between half of the orb's original size, and 100% of it's original size
this.scale = map(scaleNoise, -1, 1, 0.5, 1);

// step through "time"
this.xOff +=;
this.yOff +=;

In order for this function to run, we must also define simplex. To do so, add the following snippet anywhere before the Orb class definition:

// Create a new simplex noise instance
const simplex = new SimplexNoise();

There’s a lot of “noise” talk going on here. I realize that for some folks this will be an unfamiliar concept.

I won’t be going deep on noise in this tutorial, but I would recommend this video by Daniel Shiffman as a primer. If you are new to the concept of noise - pause this article, check out the video, and pop back!

In a nutshell, though, noise is a great way of generating _ self-similar_ random numbers. These numbers are amazing for animation, as they create smooth yet unpredictable movement.

Here’s an image from The Nature of Code showing the difference between traditional random (e.g. Math.random() and noisy random values:

A ragged, random distribution compared to a smooth noisy one

The update function here uses noise to modulate the orb’s x, y, and scale properties over time. We pick out noise values based on our xOff and yOff positions. We then use map to scale the values (always between -1 and 1) to new ranges.

The result of this? The orb will always drift within its bounds. Its size is random within constraints. The orb’s behavior is unpredictable. There are no keyframes or fixed values here.

This is all well and good, but we still can’t see anything! Let’s fix that by adding the render function to our Orb class:

render() {
// update the PIXI.Graphics position and scale values = this.x; = this.y;;

// clear anything currently drawn to graphics;

// tell graphics to fill any shapes drawn after this with the orb's fill color;
// draw a circle at { 0, 0 } with it's size set by this.radius, 0, this.radius);
// let graphics know we won't be filling in any more shapes;

render  will draw a new circle to our canvas each frame.

You may notice that the circle’s x and y values are both 0. This is because we are moving the graphics element itself, rather than the circle within it.

Why is this?

Imagine that you wanted to expand on this project, and render a more complex orb. Your new orb is now comprised of > 100 circles. It is simpler to move the entire graphics instance than to move every element within it. This may give you some performance gains, too.

Creating some orbs!

It’s time to put our Orb class to good use. Let’s create 10 brand new orb instances, and pop them into an orbs array:

// Create orbs
const orbs = [];

for (let i = 0; i < 10; i++) {
// each orb will be black, just for now
const orb = new Orb(0x000000);


We are calling app.stage.addChild to add each graphics instance to our canvas. This is akin to calling document.appendChild() on a DOM element.

Animation! Or, no animation?

Now that we have 10 new orbs, we can start to animate them. Let’s not assume everyone wants a moving background, though.

When you are building this kind of page, it is crucial to respect the user’s preferences. In our case, if the user has prefers-reduced-motion set, we will render a static background.

Here’s how we can set up a Pixi animation loop that will respect the user’s preferences:

// Animate!
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
app.ticker.add(() => {
// update and render each orb, each frame. app.ticker attempts to run at 60fps
orbs.forEach((orb) => {
} else {
// perform one update and render per orb, do not animate
orbs.forEach((orb) => {

When we call app.ticker.add(function), we tell Pixi to repeat that function at around 60 frames per second. In our case, if the user prefers reduced motion, we only run update and render our orbs once.

Once you have added the above snippet, you should see something like this in the browser:

A group of black circles

Hooray! Movement! Believe it or not, we are almost there.

Adding the blur

Our orbs are looking a little… harsh right now. Let’s fix that by adding a blur filter to our Pixi canvas. This is actually very simple and will make a huge difference to our visual output.

Pop this line below your app definition:

app.stage.filters = [new KawaseBlurFilter(30, 10, true)];

Now, if you check out the browser you should see some much softer orbs!

A group of blurry black circles

Looking great. Let’s add some color.

A Generative color palette using HSL

To introduce some color to our project, we are going to create a ColorPalette class. This class will define a set of colors we can use to fill in our orbs but also style the wider page.

I always use HSL when working with color. It’s more intuitive than hex and lends itself rather well to generative work. Here’s how:

class ColorPalette {
constructor() {

setColors() {
// pick a random hue somewhere between 220 and 360
this.hue = ~~random(220, 360);
this.complimentaryHue1 = this.hue + 30;
this.complimentaryHue2 = this.hue + 60;
// define a fixed saturation and lightness
this.saturation = 95;
this.lightness = 50;

// define a base color
this.baseColor = hsl(this.hue, this.saturation, this.lightness);
// define a complimentary color, 30 degress away from the base
this.complimentaryColor1 = hsl(
// define a second complimentary color, 60 degrees away from the base
this.complimentaryColor2 = hsl(

// store the color choices in an array so that a random one can be picked later
this.colorChoices = [

randomColor() {
// pick a random color
return this.colorChoices[~~random(0, this.colorChoices.length)].replace(

setCustomProperties() {
// set CSS custom properties so that the colors defined here can be used throughout the UI'--hue', this.hue);

We are picking 3 main colors. A random base color, and two complimentary. We pick our complementary colors by rotating the hue 30 and 60 degrees from the base.

We then set the 3 hues as custom properties in the DOM and define a randomColor function. randomColor returns a random Pixi-compatible HSL color each time it is run. We will use this for our orbs.

Let’s define a ColorPalette instance before we create our orbs:

const colorPalette = new ColorPalette();

We can then give each orb a random fill on creation:

const orb = new Orb(colorPalette.randomColor());

If you check the browser, you should now see some color!

A group of colorful, blurry circles

If you inspect the root html element in the DOM, you should also see some custom properties have been set. We are now ready to add some markup and styles for the page.

Building the rest of the page

Awesome! So our animation is complete. It looks great and is running real fast thanks to Pixi. Now we need to build the rest of the landing page.

Adding the markup

First of all, let’s add some markup to our HTML file:

<!-- Overlay -->
<div class="overlay">
<!-- Overlay inner wrapper -->
<div class="overlay__inner">
<!-- Title -->
<h1 class="overlay__title">
Hey, would you like to learn how to create a
<span class="text-gradient">generative</span> UI just like this?
<!-- Description -->
<p class="overlay__description">
In this tutorial we will be creating a generative “orb” animation using
pixi.js, picking some lovely random colors, and pulling it all together in
a nice frosty UI.
<strong>We're gonna talk accessibility, too.</strong>
<!-- Buttons -->
<div class="overlay__btns">
<button class="overlay__btn overlay__btn--transparent">
Tutorial out Feb 2, 2021
<button class="overlay__btn overlay__btn--colors">
<span>Randomise Colors</span>
<span class="overlay__btn-emoji">🎨</span>

There’s nothing too crazy going on here, so I won’t dig in too much. Let’s move onto our CSS:

Adding the CSS

:root {
--dark-color: hsl(var(--hue), 100%, 9%);
--light-color: hsl(var(--hue), 95%, 98%);
--base: hsl(var(--hue), 95%, 50%);
--complimentary1: hsl(var(--hue-complimentary1), 95%, 50%);
--complimentary2: hsl(var(--hue-complimentary2), 95%, 50%);

--font-family: 'Poppins', system-ui;

--bg-gradient: linear-gradient(
to bottom,
hsl(var(--hue), 95%, 99%),
hsl(var(--hue), 95%, 84%)

* {
margin: 0;
padding: 0;
box-sizing: border-box;

html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

body {
max-width: 1920px;
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
font-family: var(--font-family);
color: var(--dark-color);
background: var(--bg-gradient);

.orb-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;

strong {
font-weight: 600;

.overlay {
width: 100%;
max-width: 1140px;
max-height: 640px;
padding: 8rem 6rem;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.375);
box-shadow: 0 0.75rem 2rem 0 rgba(0, 0, 0, 0.1);
border-radius: 2rem;
border: 1px solid rgba(255, 255, 255, 0.125);

.overlay__inner {
max-width: 36rem;

.overlay__title {
font-size: 1.875rem;
line-height: 2.75rem;
font-weight: 700;
letter-spacing: -0.025em;
margin-bottom: 2rem;

.text-gradient {
background-image: linear-gradient(
var(--base) 25%,
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
-moz-background-clip: text;
-moz-text-fill-color: transparent;

.overlay__description {
font-size: 1rem;
line-height: 1.75rem;
margin-bottom: 3rem;

.overlay__btns {
width: 100%;
max-width: 30rem;
display: flex;

.overlay__btn {
width: 50%;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.875rem;
font-weight: 600;
color: var(--light-color);
background: var(--dark-color);
border: none;
border-radius: 0.5rem;
cursor: not-allowed;
transition: transform 150ms ease;
outline-color: hsl(var(--hue), 95%, 50%);

.overlay__btn--colors:hover {
transform: scale(1.05);
cursor: pointer;

.overlay__btn--transparent {
background: transparent;
color: var(--dark-color);
border: 2px solid var(--dark-color);
border-width: 2px;
margin-right: 0.75rem;
outline: none;

.overlay__btn-emoji {
margin-left: 0.375rem;

@media only screen and (max-width: 1140px) {
.overlay {
padding: 8rem 4rem;

@media only screen and (max-width: 840px) {
body {
padding: 1.5rem;

.overlay {
padding: 4rem;
height: auto;

.overlay__title {
font-size: 1.25rem;
line-height: 2rem;
margin-bottom: 1.5rem;

.overlay__description {
font-size: 0.875rem;
line-height: 1.5rem;
margin-bottom: 2.5rem;

@media only screen and (max-width: 600px) {
.overlay {
padding: 1.5rem;

.overlay__btns {
flex-wrap: wrap;

.overlay__btn {
width: 100%;
font-size: 0.75rem;
margin-right: 0;

.overlay__btn:first-child {
margin-bottom: 1rem;

The key part of this stylesheet is defining the custom properties in :root. These custom properties make use of the values we set with our ColorPalette class.

Using the 3 hue custom properties defined already, we create the following:

  • --dark-color - To use for all our text and primary button styles,  this is almost black with a hint of our base hue. This helps make our color palette feel coherent.
  • --light-color - To use in place of pure white. This is much the same as the dark color, almost white with a hint of our base hue.
  • --complimentary1 - Our first complimentary color, formatted to CSS friendly HSL.
  • --complimentary2 - Our second complementary color, formatted to CSS friendly HSL.
  • --bg-gradient - A subtle linear gradient based on our base hue. We use this for the page background.

We then apply these values throughout our UI. For button styles, outline colors, even a gradient text effect.

**A note on accessibility **

In this tutorial, we are almost setting our colors and letting them run free. In this case, we should be ok given the design choices we have made. In production, though, always make sure you meet at least WCAG 2.0 color contrast guidelines.

Randomising the colors in real-time

Our UI and background animation are now complete. It’s looking great, and you will see a new color palette/orb animation each time you refresh the page.

It would be good if we could randomize the colors without refreshing, though. Luckily, thanks to our custom properties/color palette setup, this is simple.

Add this small snippet to your JavaScript:

.addEventListener('click', () => {

orbs.forEach((orb) => {
orb.fill = colorPalette.randomColor();

With this snippet, we are listening for a click event on our primary button. On click, we generate a new set of colors, update the CSS custom properties, and set each orb’s fill to a new value.

As CSS custom properties are reactive, our entire UI will update in real-time. Powerful stuff.

That’s all folks

Hooray, we made it! I hope you had fun and learned something from this tutorial.

Random color palettes may be a tad experimental for most applications, but there’s a lot to take away here. Introducing an element of chance could be a great addition to your design process.

You can never go wrong with a generative animation, either.