Animated Text-Reveal Effect

Text Reveal animation tutorial for Wordpress

Learn how to create an animated text-reveal effect for your WordPress projects. This copy-and-paste tutorial uses a combination of back-end PHP, JavaScript and some funky gradient styles.

It’s one of those effects that can look silly if you over-use it – I’m totally over-using it on this page just to show it off. But it’s a fun bit of bling when used on high impact headings.

importantMake sure you’re using a custom child theme so you can edit “functions.php”.

Silky
Smooth.

How it’s Going to Work

We only want to load the JavaScript script on pages that actually need the effect. We could just add the JavaScript to every page-load, but that would be lazy coding, and it would slow down pages that don’t need the effect.

There are some main parameters that drive the effect:

  • Initial delay: How long after the page loads do we wait before starting the effect (milliseconds)
  • Duration: How long does an element take to animate, from transparent to visible (milliseconds)
  • Selector: The element selector we use to pick out the elements that need the effect

We’ll store these in the PHP file and pass them into the JavaScript using wp_localize_script() – a standard way to pass data from PHP to JavaScript in WordPress. By storing the parameters in the PHP file (instead of in the JavaScript file) we can change the parameters on a per-content basis using custom logic..

CSS Text Gradients

The fade-in-effect is going to use background clipping inline styles, but we’re going to change the style at regular intervals using JavaScript, to create the animation. We’re effectively going to change the end percentage of the linear-gradient over the animation’s duration.

Gradient Text 1

Gradient Text 2

/* Rainbow gradient */
h2.demo-gradient-1 {
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
	background-image: linear-gradient(to right, red 0, yellow 20%, green 40%, cyan 60%, blue 80%, magenta 100%);
	margin: 0 0 0.5em;
}

/* Black-to-transparent gradient */
h2.demo-gradient-2 {
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
	background-image: linear-gradient(to right, black 0, transparent 90%);
	margin: 0;
}

Animating the Gradients

First thing we need to do in the JavaScript code is find all the elements that match our selector. For each of these elements we’ll create a “fader”, which is just a simple object to track the progress of an element’s reveal effect. The fader objects look like this:

fader = {
	element:    // The DOM element that we're animating
	duration:   // Number of milliseconds the reveal effect will last for
	startTime:  // Timestamp when this element stared animating (null, if it has not started yet)
	isActive:   // Is this element part-way through animating right now?
	isFinished: // Has this element been fully revealed?
	colour:     // The final colour of this element
};

We’ll use an IntersectionObserver to keep track of when the elements come into view in the browser window. When an element comes into view, we can begin animating it by setting isActive = true.

While one or more fader objects has isActive = true, we’ll keep calling window.requestAnimationFrame() to render another frame.

The code is well commented, but if you have any questions about it – ask in the comments.

Let’s Write the Code

In your custom child theme, create two files, called “wpt-text-reveal.php” and “wpt-text-reveal.js”. Edit wpt-text-reveal.php and paste the following code into it:

<?php

/**
 * WP Tutorials : Text Reveal Effect (WPTTRE)
 *
 * https://wp-tutorials.tech/refine-wordpress/animated-text-reveal-effect/
 */

defined('WPINC') || die();

const WPTTRE_SELECTOR = 'h1, article p, article h2';
const WPTTRE_DELAY = 200;
const WPTTRE_DURATION = 1000;
const WPTTRE_COLOUR = 'black';

/**
 * Return an empty string if you don't the effect to run on page-load.
 */
function wpttre_get_selector() {
	global $wpttre_selector;

	if (is_null($wpttre_selector)) {
		$wpttre_selector = (string) apply_filters('wpttre_selector', WPTTRE_SELECTOR);
	}

	return $wpttre_selector;
}

/**
 * If a text-reveal element selector is returned by wpttre_get_selector(),
 * enqueue the text-reveal JS file.
 */
function wpttre_enqueue_scripts() {
	$theme_version = wp_get_theme()->get('Version');
	$base_uri = get_stylesheet_directory_uri();

	if (!empty($selector = wpttre_get_selector())) {
		$handle = 'wpttre';

		wp_enqueue_script(
			$handle,
			$base_uri . '/wpt-text-reveal.js',
			null, // No script dependencies here.
			$theme_version
		);

		wp_localize_script(
			$handle,
			'wptTextReveal', // The name of our JavaScript object
			array(
				'delay' => WPTTRE_DELAY,
				'duration' => WPTTRE_DURATION,
				'selector' => $selector,
				'colour' => WPTTRE_COLOUR,
			)
		);
	}
}
add_action('wp_enqueue_scripts', 'wpttre_enqueue_scripts');

Next, open your child theme’s functions.php and add the following couple of lines:

// WPT Text Reveal Effect
require_once dirname(__FILE__) . '/wpt-text-reveal.php';

We’ve created a small function called wpttre_get_selector() that returns the selector used to find our text-reveal elements. By default, this just returns what’s in WPTTRE_SELECTOR, but we can hook our wpttre_selector filter to override the default selector, or return an empty string if we don’t want the text-reveal effect to be loaded.

The standard wp_enqueue_scripts action hook lets us enqueue our JavaScript, but we only do that if wpttre_get_selector() returns something other than an empty string,

The JavaScript Bit

Edit wpt-text-reveal.js and paste the following into it. it might seem like a lot of code, but there’s not much too it when you remove the comments and diagnostics.

/**
 * wpt-text-reveal.js
 * 
 * https://wp-tutorials.tech/refine-wordpress/animated-text-reveal-effect/
 */
document.addEventListener('DOMContentLoaded', function() {
	'use strict';

	// Diagnostics
	// console.log('WPT Text Reveal Effect : load');

	if (typeof wptTextReveal !== 'undefined') {

		// Diagnostics
		// console.log('WPT Text Reveal Effect : init');

		// wptTextReveal has been created by wp_localize_script(). Add some local
		// state properties to it.
		wptTextReveal.delay = parseInt(wptTextReveal.delay);
		wptTextReveal.duration = parseInt(wptTextReveal.duration);
		wptTextReveal.activeFaderCount = 0;
		wptTextReveal.finishedFaderCount = 0;
		wptTextReveal.faders = [];
		wptTextReveal.observer = null;

		//
		// Main entry point.
		//
		if (wptTextReveal.delay <= 0) {
			createFaders();
		} else {
			setTimeout(createFaders, wptTextReveal.delay);
		}

		// Diagnostics - dump the contents of wptTextReveal to the console.
		// console.log(wptTextReveal);

		/**
		 * Create our fader objects based on the elements that match our selector.
		 * Add the elements to in intersection observer.
		 */
		function createFaders() {
			wptTextReveal.observer = new IntersectionObserver(observedElementsChanged, { threshold: 1 });

			const elements = document.querySelectorAll(wptTextReveal.selector);
			elements.forEach((element) => {
				const fader = {
					element: element,
					duration: wptTextReveal.duration,
					startTime: null,
					isActive: false,
					isFinished: false,
					colour: wptTextReveal.colour
				};

				wptTextReveal.faders.push(fader);

				wptTextReveal.observer.observe(element);
			});

			// Diagnostics
			// console.log(`Faders: ${wptTextReveal.faders.length}`);
		}

		/**
		 * This is called when any of our observed elements changes state...
		 * i.e. they have appeared on the screen, or they have disappeared off
		 * the screen.
		 */
		function observedElementsChanged(entries) {
			let newActiveFaderCount = 0;

			// Loop through our faders to find any that are intersecting
			// (i.e. visible on the screen), have not started animating yet and
			// have not already finished animating.
			wptTextReveal.faders.forEach((fader) => {
				const found = entries.find((entry) => {
					return entry.isIntersecting && !fader.isActive && !fader.isFinished && (entry.target === fader.element);
				});

				if (found) {
					fader.isActive = true;
					++newActiveFaderCount;
				}
			});

			// Diagnostics
			// if ((newActiveFaderCount > 0)) {
			// 	console.log(`Found new elements to start fading: ${newActiveFaderCount}`);
			// }

			if ((newActiveFaderCount > 0) && (wptTextReveal.activeFaderCount == 0)) {
				// Diagnostics
				// console.log(`Request first animation frame now`);

				window.requestAnimationFrame(animateFrame);
			}
		}

		/**
		 * For each active fader, calculate the percentage complete and set the
		 * gradient based on this percentage.
		 */
		function animateFrame(timestamp) {
			wptTextReveal.activeFaderCount = 0;
			wptTextReveal.finishedFaderCount = 0;

			wptTextReveal.faders.forEach((fader) => {
				if (fader.isActive) {
					// If it is the first time this fader is going to be rendered
					// (its initial frame), set the starting time stamp to "now".
					if (fader.startTime === null) {
						fader.startTime = timestamp;
					}

					// How much time has passed since the previous frame was rendered?
					// If it is the fader's first frame, elapsedTime will be zero.
					const elapsedTime = timestamp - fader.startTime;

					// Calculate the percentage complete to one decimal place...
					// from 0.0 to 100.0
					let endPercentage = Math.round(1000.0 * elapsedTime / fader.duration) * 0.10;

					// Sanity check.
					if (endPercentage < 0.0) {
						endPercentage = 0.0;
					}

					// Detect when this fader has reached the end of its animation
					// and clamp the percentage complete to 200.0.
					// NOTE: The animation is finished when startPercentage is 100.0
					//       and endPercentage is 200.0
					if (endPercentage >= 200.0) {
						endPercentage = 200.0;
						fader.isActive = false;
						fader.isFinished = true;
					}

					if ((endPercentage >= 0.0) && (endPercentage <= 200.0)) {
						let startPercentage = endPercentage - 100.0;
						if (startPercentage < 0.0) {
							startPercentage = 0.0;
						}

						if (!fader.isFinished) {
							fader.element.style.backgroundImage = `linear-gradient( to bottom right, ${fader.colour} ${startPercentage}%, transparent ${endPercentage}%)`;
						} else {
							// When we've finished revealing the element, "unset" our
							// inline styles to let the core styles come through.
							fader.element.style.backgroundImage = 'unset';
							fader.element.style.webkitTextFillColor = 'unset';
						}
					}

					++wptTextReveal.activeFaderCount;
				}

				if (fader.isFinished) {
					++wptTextReveal.finishedFaderCount;
				}
			});

			// Only request another animation frame from the browser if one or
			// more faders are part-way through being animated.
			if (wptTextReveal.activeFaderCount > 0) {
				window.requestAnimationFrame(animateFrame);
			} else {
				// Diagnostics
				// console.log(`No active faders. ${wptTextReveal.finishedFaderCount} of ${wptTextReveal.faders.length} have finished`);
			}
		}
	}

});

If the code appears too complicated to read through, just break it down into smaller chunks. Start at the top and you’ll see the initialisation code… where we set up our wptTextReveal object. This object is defined by wp_localize_script in the PHP file, but we add some extra properties to it in the JavaScript to track the faders.

The main entry point checks to see if we need a delay after loading, and before the animations start. If there’s no delay, we call createFaders() straight away. But, if we’ve specified a short delay, we use setTimeout() to wait and then call our createFaders() function.

The three core functions in our script:

  • createFaders() : Scan the DOM for elements we want to apply the text-reveal effect to. Add the DOM elements to our IntersectionObserver object and create the fader objects.
  • observedElementsChanged() : This is called by the observer each time one or more of our elements comes into view. We loop through the faders to see if we need to set isActive=true for any newly-visible elements that need to be animated.
  • animateFrame() : Loop through all the active faders, calculate how far through their animations they are, and set their gradients with an inline style.

tipThere are some useful diagnostic calls to console.log() – uncomment these and open your browser’s JS Console if you want to see the key stages being triggered.

Fix the Page-Load Flicker

If you run the tutorial now, you’ll see the elements you want to text-reveal are briefly visible on page-load. Then they disappear, and the animations start.

What happens is this… There is a short delay between the page loading, and the script starting to run (waiting for the DOMContentLoaded event). During this short time, all the elements on the page are rendered normally because we haven’t begun applying our inline styles yet.

Stage 1: Page load

My Heading

Element is rendered normally

Stage 2: Script initialised

My Heading

Element is 100% transparent

Stage 3: Script running

My Heading

Element is part-revealed

That’s annoying, but we can fix it. All we need to do is inject a style definition into the document’s header to hide the matched elements on page-load.

Open wpt-text-reveal.php again and add the following snippet to the end of the file:

/**
 * If a text-reveal element selector is returned by wpttre_get_selector(),
 * set those elements to transparent with a custom style definition in the
 * document header.
 */
function wpttre_wp_head() {
	if (!empty($selector = wpttre_get_selector())) {
		printf('
<style>
%s {
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-image: linear-gradient( to bottom right, %s -1px, transparent 0px);
}
</style>',
			$selector,
			esc_html(WPTTRE_COLOUR)
		);
	}
}
add_action('wp_head', 'wpttre_wp_head');

All this does is hook the wp_head action, which WordPress uses to render the contents of <head>...</head>. We inject a short <style>...</style> element with a single rule, matched against our element selector. This rule sets all the matched elements to have a gradient that starts at negative 1 pixels and finishes at zero pixels. Everything to the right of zero pixels (in the left-to-right gradient) will be transparent. You can verify this by changing transparent 0px to transparent 50% and see what happens on page-load.

Have Fun With It

There’s all sorts of fun you can have with this code. The most obvious is to play with the linear-gradient definition in the JavaScript. Rather than just having a simple start and end colours (black -> transparent), try adding extra gradient colour stops. See how it looks on different page elements – I think it works best in a big page hero section.

Have some revealing fun 😎 👍

Featured plugin

WooCommerce Flexible Product Tabs
Elegant and flexible product tabs for WooCommerce. Use the Block Editor for your main Description tab too.
Flexible Product Tabs for WooCommerce

Like This Tutorial?

Let us know

WordPress plugins for developers

Leave a comment