WordPress Developer Blog

How to create an animated timeline plugin

How to create an animated timeline plugin

Have you ever seen an animated timeline of events? As the visitor scrolls down and new events enter the viewport, the timeline animates any additional events into view. A linear visual, like a timeline, can tell a story. The events may represent critical milestones in a person or company’s history. Similarly, animation can help reinforce storytelling and appeal to a visitor’s creative interest.

I want to walk you through creating a unique timeline experience. You will learn to build it with blocks, register some custom CSS for animation, and use JavaScript to observe intersecting elements in the viewport. Of course, not everybody wants to experience elements moving on a page, so we will show you how you can opt to turn off animations.

Below is a video that captures the final timeline appearance that you’ll create:

Create the timeline with blocks

First, you’ll build the timeline layout and appearance with blocks. I ended up using the following blocks:

  • A Group block with a custom timeline CSS class assigned.
    • A Columns block with three nested columns.
      1. The first Column block represents the date of the event. Example: “2024, April 2”
      2. The second Column block represents a visual timeline divider. Iused block locking to prevent the editor from accidentally removing or moving this block.
      3. The third Column block is where the content will live for each event on the timeline.

Below is a visual, breaking  down the blocks that make up the final Vertical Timeline pattern:

Next, consider modifying the pattern’s inner content to assign critical animation classes, which we will accomplish within your plugin.

Animated timeline plugin setup

Let’s create a plugin to package everything. This will also allow you to reuse it and drop it into any project.

You’ll start by registering your plugin and setting up the file structure. Create a new animated-timeline directory in your wp-content/plugins, and add all of the files you’ll need:

  • animated-timeline/: your plugin folder
    • /assets/scripts/core-blocks/group–animated-timeline.js: where you will use JavaScript to watch for intersecting elements in the viewport.
    • /assets/styles/core-blocks/group–animated-timeline.css: where you will use CSS to transition, delay, and animate critical elements.
    • animated-timeline.php: the main plugin file and where you will place most of the hooks.

Now, add your WordPress plugin’s header in the animated-timeline.php file:

<?php
/**
* Plugin Name: Animated Timeline
* Description: Extends the Group block for an animating timeline effect.
* Requires at least: 6.5
* Requires PHP: 7.4
* Version: 1.0.0
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: animated-timeline
*/

if ( ! defined( 'ABSPATH' ) ) {
   exit; // Exit if accessed directly.
}

Go ahead and activate your new plugin.

HTML Tag Processor and block filters

One critical piece you need to accomplish is filtering the Group block’s inner content. You need to check if any Group blocks have a custom class assigned, ensure it is .animated-timeline, and then process its inner block content.

You could utilize either of these handy filters to modify the block’s output: render_block and render_block_{$this->name}. However, as the aim is to target customizations to the Group block, you can use the dynamic render_block_core/group variation.
Below is the code to add to your plugin’s animated-timeline.php file:

/**
* Modify the core Group block.
*
* @param string $block_content The block content about to be rendered.
*
* @return string               The maybe modified block content.
*/
function animated_timeline_filter_group_content( $block_content ) {
   $processor = new WP_HTML_Tag_Processor( $block_content );
   $counter   = 0;

   // Check for the presence of the animated-timeline class.
   if ( ! $processor->next_tag( array( 'class_name' => 'animated-timeline' ) ) ) {
       return $block_content;
   }

   // Loop through each child block with the class name 'wp-block-column'.
   while ( $processor->next_tag( array( 'class_name' => 'wp-block-column' ) ) ) {
       $processor->add_class( 'animated__item' );
       ++$counter;

       switch ( $counter ) {
           case 1:
               $processor->add_class( 'animated__item--first' );
               break;
           case 2:
               $processor->add_class( 'animated__item--line' );
               break;
           case 3:
               $processor->add_class( 'animated__item--last' );
               $counter = 0;
               break;
       }
   }

   $block_content = $processor->get_updated_html();

   // Enqueue the custom script and style.
   wp_enqueue_script( 'animated-timeline-script' );
   wp_enqueue_style( 'animated-timeline-style' );

   // Return the maybe modified block content.
   return $block_content;
}
add_filter( 'render_block_core/group', 'animated_timeline_filter_group_content', 10 );

The HTML Tag Processor is a powerful tool that helps cycle through the nested block content and assign classes to each Column block. You may also notice that the code uses wp_enqueue_script() and wp_enqueue_style() to enqueue custom CSS and JavaScript files, but only if the Group block with the .timeline class is on the front end of the site. This ensures that the assets are only loaded when a Group block with the .animated-timeline class is output on the page.

Register custom CSS and JavaScript files

In the previous step, you called wp_enqueue_script( 'animated-timeline-script' ) and wp_enqueue_style( 'animated-timeline-style' ). You need to register these JavaScript and CSS files so that WordPress knows what to load. Add the following code to your animated-timeline.php to register the group--animated-timeline.js and group--animated-timeline.css files:

/**
* Registers the custom script and style for the timeline plugin.
*/
function animated_timeline_register_scripts() {

   // Register the custom script to be used later.
   wp_register_script(
       'animated-timeline-script',
       plugin_dir_url( __FILE__ ) . '/assets/scripts/core-blocks/group--animated-timeline.js',
       array(),
       '1.0.0',
       true
   );

   // Register the custom style to be used later.
   wp_register_style(
       'animated-timeline-style',
       plugin_dir_url( __FILE__ ) . '/assets/styles/core-blocks/group--animated-timeline.css',
       array(),
       '1.0.0',
   );
}
add_action( 'wp_enqueue_scripts', 'animated_timeline_register_scripts' );

Add the animation styles

Now add the custom CSS to the /assets/styles/core-blocks/group--animated-timeline.css file to handle animations:

/* Establish the positioning context for the timeline. */
.animated-timeline > div {
	position: relative;
}

.animated__item {
	visibility: hidden;
}

.animated__item.loaded {
	visibility: visible;
}

/* First column - initial state */
.animated-timeline .animated__item--first {
	opacity: 0;
	transform: translateY(25px);
	transition: transform 0.5s, opacity 0.5s;
	transition-delay: 0.3s;
}

/* Last column - initial state */
.animated-timeline .animated__item--last {
	opacity: 0;
	transform: translateY(45px);
	transition: transform 0.7s, opacity 0.8s;
	transition-delay: 0.5s;
}

/* First and last column - loaded state */
.animated-timeline .animated__item--first.loaded,
.animated-timeline .animated__item--last.loaded {
	opacity: 1;
	transform: translateY(0);
}

/**
 * Vertical line animation
 * The vertical line is a pseudo-element of the middle column.
 */

/* Establish positioning context */
.animated-timeline .animated__item--line {
	position: relative;
	visibility: hidden;
}

/* Vertical line - initial state */
.animated-timeline .animated__item--line::before {
	background-color: inherit;
	content: "";
	display: block;
	height: 1px;
	inset: 0;
	opacity: 0;
	overflow: hidden;
	position: absolute;
	transition: height 1.5s, opacity 0.1s;
	transition-delay: 0.1s;
	transition-origin: top;
	visibility: hidden;
	width: 100%;
	z-index: -1;
}

/* Vertical line - loaded state */
.animated-timeline .animated__item--line.loaded::before {
	height: 100%;
	opacity: 1;
	visibility: visible;
}

/* Middle column - inner text - initial state */
.animated-timeline .animated__item--line > p {
	opacity: 0;
	transition: opacity 0.5s;
	transition-delay: 1.15s;
	visibility: hidden;
}

/* Middle column - inner text - loaded state */
.animated-timeline:not(.animated-timeline--circles) .animated__item--line.loaded > p {
	opacity: 1;
	visibility: visible;
}

/**
 * Circle timeline
 * The circle timeline is a variation of the default timeline.
 */
.animated-timeline--circles .animated__item--line > p {
	opacity: 1;
	position: relative;
	visibility: hidden;
}

/* Create the circles */
.animated-timeline--circles .animated__item--line > p::after,
.animated-timeline--circles .animated__item--line > p::before {
	background-color: inherit;
	border-radius: 50%;
	content: "";
	display: block;
	height: 1rem;
	left: calc(50% - 0.5rem);
	opacity: 0;
	position: absolute;
	top: calc(50% - 0.5rem);
	transition: opacity 0.4s, transform 0.6s;
	visibility: hidden;
	width: 1rem;
}

/* Background circle - initial state */
.animated-timeline--circles .animated__item--line > p::after {
	background: none;
	box-shadow: 0 0 0 4px currentColor;
	transform: scale(0);
	transition-delay: 1.2s;
	z-index: 1;
}

/* Background circle - loaded state */
.animated-timeline--circles .animated__item--line.loaded > p::after {
	opacity: 0.4;
	transform: scale(1);
	visibility: visible;
}

/* Foreground circle - initial state */
.animated-timeline--circles .animated__item--line > p::before {
	transition-delay: 1s;
	z-index: 2;
}

/* Foreground circle - loaded state */
.animated-timeline--circles .animated__item--line.loaded > p::before {
	opacity: 1;
	visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
	.animated-timeline *,
	.animated-timeline *::after,
	.animated-timeline *::before {
		opacity: 1 !important;
		transition: none !important;
		visibility: visible !important;
	}
}

You may notice that I’ve included two timeline style variations for you to use:

  1. Defaultanimated-timeline: the default appearance and style.
  2. Circlesanimated-timeline animated-timeline--circles: changes the dividing line embellishment to a circular, dot-like treatment.

You can use whichever you prefer or even create a new variation. Just be sure to add either animated-timeline or animated-timeline animated-timeline--circles in the Advanced -> Additional CSS Classes field in the editor.

The last block of CSS includes an @media (prefers-reduced-motion) declaration. This allows visitors to utilize their device’s setting to minimize any non-essential motion.

Add the animation JavaScript

You have your styling in place to animate the different classes. Now you need to add a touch of JavaScript to watch the viewport for scrolling and attach the classes to animate the targeted elements.
Place the following code in the /assets/styles/core-blocks/group--animated-timeline.js file:

/**
* Initializes an Intersection Observer to add the 'loaded' class to elements when they become visible in the viewport.
* The Intersection Observer is set up to observe elements with the class 'animated__item'.
*
* @listens DOMContentLoaded
*/
document.addEventListener( 'DOMContentLoaded', () => {
   const els = document.querySelectorAll( '.animated__item' );

   const observerOptions = {
       root: null,
       rootMargin: '0px',
       threshold: 0.33,
   };

   /**
    * Callback function for the Intersection Observer.
    * Adds the 'loaded' class to the target element if it is intersecting.
    *
    * @param {IntersectionObserverEntry[]} entries - An array of IntersectionObserverEntry objects.
    */
   function observerCallback( entries ) {
       entries.forEach( ( entry ) => {
           if ( entry.isIntersecting ) {
               entry.target.classList.add( 'loaded' );
           }
       } );
   }

   const observer = new IntersectionObserver(
       observerCallback,
       observerOptions
   );

   els.forEach( ( el ) => observer.observe( el ) );
} );

The above code utilizes JavaScript’s IntersectionObserver API to watch for intersecting elements within the viewport and attaches a .loaded class to any targeted .animated__item elements.

Block pattern registration

You should now have the animated timeline working. However, there is the consideration of how to organize your files. Remember, that the final functionality relies on your plugin (as it currently stands) and a block pattern. There are a few ways you can potentially include the pattern:

  • Register and include it in your plugin (recommended).
  • Register and include it in a custom theme (just place a copy of the pattern in your theme’s patterns/ directory).
  • Add it (as needed) from the Pattern Directory when creating new posts / pages.

If you prefer to register the pattern within your final plugin, this is what you should place in your animated-timeline.php file:

/**
 * Registers a block pattern for the timeline plugin.
 *
 * This function registers a block pattern for the timeline plugin. It checks if the pattern file exists and then registers the pattern using the `register_block_pattern` function.
 */
function animated_timeline_register_block_pattern() {
	$pattern_file = plugin_dir_path( __FILE__ ) . '/patterns/animated-timeline.php';

	if ( ! file_exists( $pattern_file ) ) {
		return;
	}

	register_block_pattern(
		'animated-timeline/animated-timeline',
		require $pattern_file
	);
}
add_action( 'init', 'animated_timeline_register_block_pattern' );

The animated_timeline_register_block_pattern() function above is checking for the /patterns/animated-timeline.php pattern, which you need to include in your plugin. You can find the final animated-timeline.php pattern on this GitHub repo. Note: the pattern references some images, which are also located relative to the pattern (see assets/images/) in the GitHub repo. You will want to include those for completeness.

Wrapping up

With a little planning and the right WordPress APIs, you can customize a lot, and benefit from the amazing new features ahead by utilizing WordPress core functionality.

I hope you learned how to break things down and considered how you might address your next potential client project that offers similar visual components.

This is the final GitHub repo with the complete plugin code: github.com/colorful-tones/animated-timeline-plugin. Please feel free to fork it and customize it. You can share how you use it in projects in the comments area. 

Props to @bph, @greenshady@dmsnell and @webcommsat for feedback and review of this post.

Categories: , ,

Leave a Reply