Introducing beings . new website coming soon

  • Get Support
  • Services
  • Get a Quote
Get A Quote

Interactive liquid gradient background with Three.js, a step by step tutorial

Looking for a hero background that feels alive, not just a flat gradient or a stock video loop. In this tutorial we will build an interactive liquid gradient background with Three.js and a custom shader, complete with cursor driven distortion, film grain, and color schemes you can toggle in the UI.

The effect is perfect for landing pages, portfolio sites, and experimental brands that want a premium, motion driven feel without video files. As the user moves their cursor, ripples push through a smooth, high contrast gradient that never stops moving.

You can view the live demo and full source code on CodePen here:

Interactive liquid gradient background demo on CodePen

What we are going to build

The final result is a full screen WebGL canvas that sits behind your content. On top of it, you can place a large heading, a compact color scheme switcher, and a footer link, plus a custom circular cursor that tracks the mouse.

  • The gradient is powered by a fragment shader that blends several moving color centers for added depth.
  • A small offscreen canvas stores a touch texture, this is updated as you move your cursor, then sampled in the shader to create distortion.
  • Color scheme buttons allow you to switch between different palettes instantly.
  • A custom cursor emphasizes the interactive feel and aligns with the premium visual style.

Prerequisites

To follow along you should be comfortable with basic HTML, CSS, and JavaScript, plus the basic concepts of Three.js such as scenes, cameras, and meshes. You do not need deep shader experience, we will walk through the important parts of the fragment shader.

  • Working knowledge of modern JavaScript
  • Some experience with Three.js
  • A code editor and a simple local server or CodePen

High level architecture

Before diving into code, it helps to understand the moving parts behind the effect.

  • TouchTexture class records cursor movement into a small canvas texture.
  • GradientBackground class builds the full screen plane and shader material that renders the gradient.
  • App class wires together the Three.js renderer, camera, scene, uniforms, and animation loop.
  • UI layer handles the heading, buttons, footer link, and custom cursor using regular HTML and CSS.

This separation keeps the WebGL logic focused and allows you to treat the gradient background as a reusable component for future projects.

Step 1, Create the HTML structure

The HTML is intentionally simple. The WebGL canvas is created in JavaScript, so the document only needs content elements and containers for UI.

<body>
  <h1 class="heading">Liquid Gradient</h1>

  <div class="color-controls">
    <button class="color-btn active" data-scheme="1">Scheme 1</button>
    <button class="color-btn" data-scheme="2">Scheme 2</button>
    <button class="color-btn" data-scheme="3">Scheme 3</button>
  </div>

  <footer class="footer">
    <a href="https://madebybeings.com"
       target="_blank"
       rel="noopener noreferrer">
      Made By Beings
    </a>
  </footer>

  <div class="custom-cursor" id="customCursor"></div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  <script src="app.js"></script>
</body>

Why this structure works well for a hero section.

  • The heading is centered using CSS transforms, which gives an instant hero layout when combined with a full screen background.
  • The color controls live in the top right corner and do not interfere with the main message.
  • The footer link showcases your brand while keeping the composition balanced.
  • The custom cursor is a dedicated element, so you can animate and style it independently from the browser cursor.

Step 2, Style the hero and overlay content

The CSS handles full viewport sizing, stacking order, and typography. The WebGL canvas is positioned behind the content with a low z index, while the heading, controls, and footer sit on top.

body {
  overflow: hidden;
  font-family: sans-serif;
  cursor: none;
}

#webGLApp {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: 1;
}

.heading {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 10;
  color: white;
  text-align: center;
  font-family: 'Syne', sans-serif;
  font-size: clamp(3.5rem, 9vw, 9rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1;
  pointer-events: none;
}

Notice the use of pointer-events: none on the heading. This allows mouse events to pass through to the WebGL canvas, so the gradient continues to react even when the cursor is over text.

The color controls use a glassy style that matches the modern gradient feel.

.color-controls {
  position: fixed;
  top: 2rem;
  right: 2rem;
  z-index: 10;
  display: flex;
  gap: 1rem;
}

.color-btn {
  padding: 0.75rem 1.5rem;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.3);
  color: white;
  font-family: 'Syne', sans-serif;
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  cursor: pointer;
  transition: all 0.3s ease;
  backdrop-filter: blur(10px);
}

The custom cursor is a bordered circle with a small solid dot in the center. By hiding the native cursor and updating the position of this element on every mousemove, you get a smooth, tailored pointer that suits the motion design.

Step 3, Set up the WebGL scene with Three.js

The App class creates the renderer, camera, scene, and main animation loop. It also initializes the gradient background and touch texture.

class App {
  constructor() {
    this.renderer = new THREE.WebGLRenderer({
      antialias: true,
      powerPreference: "high-performance",
      alpha: false,
      stencil: false,
      depth: false
    });

    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(window.devicePixelRatio);
    document.body.appendChild(this.renderer.domElement);
    this.renderer.domElement.id = "webGLApp";

    this.camera = new THREE.PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      0.1,
      10000
    );
    this.camera.position.z = 50;

    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x0a0e27);

    this.clock = new THREE.Clock();

    this.touchTexture = new TouchTexture();
    this.gradientBackground = new GradientBackground(this);
    this.gradientBackground.uniforms.uTouchTexture.value =
      this.touchTexture.texture;

    this.init();
  }

  init() {
    this.gradientBackground.init();
    this.tick();

    window.addEventListener("resize", () => this.onResize());
    window.addEventListener("mousemove", ev => this.onMouseMove(ev));
    window.addEventListener("touchmove", ev => this.onTouchMove(ev));
  }

  tick() {
    this.render();
    requestAnimationFrame(this.tick.bind(this));
  }

  render() {
    const delta = Math.min(this.clock.getDelta(), 0.1);
    this.touchTexture.update();
    this.gradientBackground.update(delta);
    this.renderer.render(this.scene, this.camera);
  }
}

Two details here improve performance and visual quality.

  • Power preference is set to high performance, which signals the browser to favor dedicated graphics hardware when available.
  • The delta time passed into update is clamped, so long browser pauses do not cause sudden jumps in the animation.

Step 4, Create the liquid gradient shader

The GradientBackground class builds a full screen plane and applies a ShaderMaterial that holds all the uniforms we need for time, resolution, colors, and touch texture.

class GradientBackground {
  constructor(sceneManager) {
    this.sceneManager = sceneManager;
    this.uniforms = {
      uTime: { value: 0 },
      uResolution: {
        value: new THREE.Vector2(window.innerWidth, window.innerHeight)
      },
      uColor1: { value: new THREE.Vector3(0.945, 0.353, 0.133) },
      uColor2: { value: new THREE.Vector3(0.0, 0.098, 0.247) },
      uColor3: { value: new THREE.Vector3(0.945, 0.353, 0.133) },
      uColor4: { value: new THREE.Vector3(0.0, 0.098, 0.247) },
      uColor5: { value: new THREE.Vector3(0.945, 0.353, 0.133) },
      uColor6: { value: new THREE.Vector3(0.0, 0.098, 0.247) },
      uSpeed: { value: 1.2 },
      uIntensity: { value: 1.8 },
      uTouchTexture: { value: null },
      uGrainIntensity: { value: 0.08 }
    };
  }

The vertex shader simply passes through UV coordinates.

vertexShader: `
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix
      * modelViewMatrix
      * vec4(position, 1.0);
  }
`

The fragment shader does the interesting work. It computes several moving centers, measures distance from each center, and uses smooth curves to blend between up to six colors. It also rotates UVs to add layered radial gradients, mixes in a dark navy base, and applies a subtle grain function for a film like texture. Finally, it samples the touch texture and uses its red, green, and blue channels to distort UV coordinates and drive ripple intensity.

If shader code feels intimidating, start by adjusting values such as uSpeed, uIntensity, and uGrainIntensity. Small changes to these uniforms can drastically change the mood of the gradient, from soft and slow to intense and energetic.

Step 5, Track cursor movement with a touch texture

The TouchTexture class converts mouse movement into a texture that the shader can read. Each time the mouse moves, a new point is added with a position, velocity, age, and force. As time passes, old points fade and new ones appear, which creates streaks and waves when applied to UV distortion.

class TouchTexture {
  constructor() {
    this.size = 64;
    this.width = this.height = this.size;
    this.maxAge = 64;
    this.radius = 0.25 * this.size;
    this.speed = 1 / this.maxAge;
    this.trail = [];
    this.last = null;
    this.initTexture();
  }

  addTouch(point) {
    let force = 0;
    let vx = 0;
    let vy = 0;
    const last = this.last;

    if (last) {
      const dx = point.x - last.x;
      const dy = point.y - last.y;
      if (dx === 0 && dy === 0) return;

      const dd = dx * dx + dy * dy;
      const d = Math.sqrt(dd);
      vx = dx / d;
      vy = dy / d;

      force = Math.min(dd * 20000, 2.0);
    }

    this.last = { x: point.x, y: point.y };
    this.trail.push({ x: point.x, y: point.y, age: 0, force, vx, vy });
  }

The update method clears the canvas, advances each point in the direction of its velocity, increases its age, and draws it as a blurred circle with intensity based on age and force. The result is a compact texture that captures the pressure and direction of user movement, which the shader can use to bend the gradient in a fluid way.

Step 6, Wire up color schemes and the custom cursor

To make the effect more usable in real projects, the App class exposes a setColorScheme method. Each scheme defines two colors, which are copied into all six color uniforms so you get strong contrast without managing many palettes.

this.colorSchemes = {
  1: {
    color1: new THREE.Vector3(0.945, 0.353, 0.133),
    color2: new THREE.Vector3(0.0, 0.098, 0.247)
  },
  2: {
    color1: new THREE.Vector3(0.945, 0.353, 0.133),
    color2: new THREE.Vector3(0.757, 0.780, 0.780)
  },
  3: {
    color1: new THREE.Vector3(0.6, 0.3, 0.9),
    color2: new THREE.Vector3(0.2, 0.8, 0.9)
  }
};

Buttons in the UI call this method on click, and a simple active class toggle provides visual feedback.

const colorButtons = document.querySelectorAll('.color-btn');

colorButtons.forEach(btn => {
  btn.addEventListener('click', () => {
    const scheme = parseInt(btn.dataset.scheme, 10);
    app.setColorScheme(scheme);

    colorButtons.forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
  });
});

The custom cursor code tracks the real mouse position, then sets the position of the cursor element on every frame. On hover over interactive elements such as the footer link, the cursor temporarily grows in size for a simple micro interaction that reinforces click targets.

Using the liquid gradient in a real hero section

To use this effect in a live site, you can treat it as a full screen hero layer and place your marketing content on top. Here are a few practical tips for integration.

  • Keep the heading short and bold, the background is visually rich, so the copy should be very clear.
  • Use pointer-events: none on purely decorative text and layout elements that sit above the canvas.
  • Place key calls to action in solid buttons with enough contrast over the gradient, ideally with a semi transparent or blurred backing for accessibility.
  • Test the effect on different screen sizes, check that performance feels smooth on typical laptop hardware and mobile devices.

If you want more control in WordPress, you can wrap the whole hero in a group block, add your heading and buttons as regular blocks, and include the WebGL script either via a custom theme script or a code snippet plugin in the footer.

Performance and accessibility notes

WebGL effects can be heavy if not tuned carefully. This setup keeps things lean in a few ways.

  • The touch texture uses a small canvas size, so sampling it in the shader stays inexpensive.
  • The scene contains a single plane mesh, there are no extra lights or complex geometry.
  • The clock delta is clamped, which helps avoid sudden jumps if the tab is inactive.
  • Depth and stencil buffers are disabled on the renderer, which reduces memory overhead.

For accessibility, consider offering a toggle to reduce motion for users who prefer calmer interfaces. You can read the prefers reduced motion media query in CSS or JavaScript and lower the speed and intensity uniforms, or pause the animation completely.

Where to go next

This interactive liquid gradient is a strong foundation for a signature visual on your site. From here you can experiment with different palettes, slower and faster motion, logo reveals, or even layering typography directly inside the Three.js scene.

To dig into every line of the implementation and fork your own version, open the full demo here.

View the interactive liquid gradient source code on CodePen

Drop it into your next hero section, match the palette to your brand, and you have a live, tactile background that sets your page apart the moment it loads.