Step-by-Step Guide to building a custom custom analytics tracker block for Gutenberg using Vanilla CSS shadow DOM style layers
Gutenberg Block Development: Custom Analytics Tracker with Vanilla CSS Shadow DOM Style Layers
This guide details the construction of a custom Gutenberg block for WordPress that acts as an analytics tracker. We will leverage Vanilla CSS and the Shadow DOM to encapsulate styles, preventing conflicts with the theme or other plugins. This approach ensures predictable rendering and maintainability, crucial for production environments.
I. Project Setup and Block Registration
First, we need to set up a basic WordPress plugin structure and register our custom block. This involves creating a plugin directory, a main plugin file, and a JavaScript file for the block’s editor interface.
A. Plugin Structure
Create a new directory in your WordPress `wp-content/plugins/` folder, for example, `custom-analytics-tracker`. Inside this directory, create two files:
- `custom-analytics-tracker.php` (main plugin file)
- `build/index.js` (compiled JavaScript for the block)
B. Main Plugin File (`custom-analytics-tracker.php`)
This file will register the block type and enqueue necessary scripts. We’ll use the `register_block_type` function.
<?php
/**
* Plugin Name: Custom Analytics Tracker
* Description: A custom Gutenberg block for tracking analytics.
* Version: 1.0.0
* Author: Your Name
* License: GPL-2.0-or-later
* Text Domain: custom-analytics-tracker
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Registers the block using the metadata loaded from the `block.json` file.
* Behind the scenes, it registers also all assets so they can be enqueued
* through the block editor in the corresponding context.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function custom_analytics_tracker_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_analytics_tracker_tracker_block_init' );
C. Block Metadata (`block.json`)
Create a `block.json` file in the root of your plugin directory. This file describes the block to WordPress, including its name, category, and script/style dependencies.
{
"apiVersion": 2,
"name": "custom-analytics-tracker/tracker",
"version": "1.0.0",
"title": "Analytics Tracker",
"category": "widgets",
"icon": "chart-bar",
"description": "A custom block to track specific user interactions.",
"attributes": {
"trackingId": {
"type": "string",
"default": ""
},
"eventName": {
"type": "string",
"default": ""
}
},
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css",
"render": "file:./render.php"
}
II. Block Editor Implementation (JavaScript)
We’ll use `@wordpress/scripts` to compile our JavaScript and CSS. Install it as a development dependency:
cd wp-content/plugins/custom-analytics-tracker npm init -y npm install @wordpress/scripts --save-dev
Add a build script to your `package.json`:
{
// ... other properties
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
}
// ...
}
Now, create `src/index.js` and `src/editor.scss` (for editor styles) and `src/style.scss` (for frontend styles). Run `npm run build` to compile them into `build/index.js` and `build/index.css`.
A. `src/index.js` – Block Registration and Editor Controls
This file defines the block’s behavior in the editor. We’ll use `registerBlockType` from `@wordpress/blocks` and components from `@wordpress/block-editor` and `@wordpress/components`.
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import './editor.scss'; // Import editor styles
import './style.scss'; // Import frontend styles
registerBlockType( 'custom-analytics-tracker/tracker', {
apiVersion: 2,
title: 'Analytics Tracker',
icon: 'chart-bar',
category: 'widgets',
attributes: {
trackingId: {
type: 'string',
default: '',
},
eventName: {
type: 'string',
default: '',
},
},
edit: ( { attributes, setAttributes } ) => {
const blockProps = useBlockProps();
const { trackingId, eventName } = attributes;
return (
<>
<InspectorControls>
<PanelBody title="Analytics Settings" initialOpen={ true }>
<TextControl
label="Tracking ID"
value={ trackingId }
onChange={ ( newTrackingId ) => setAttributes( { trackingId: newTrackingId } ) }
/>
<TextControl
label="Event Name"
value={ eventName }
onChange={ ( newEventName ) => setAttributes( { eventName: newEventName } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<p>Analytics Tracker Block</p>
{ trackingId && <p>Tracking ID: { trackingId }</p> }
{ eventName && <p>Event: { eventName }</p> }
<p>Configure in the sidebar.</p>
</div>
</>
);
},
save: () => {
// The save function is not needed if using a PHP render callback.
// If you were saving HTML directly, you'd return null or the static HTML.
return null;
},
} );
B. `src/editor.scss` – Editor-Specific Styles
Styles for the block within the Gutenberg editor. These won’t affect the frontend.
.wp-block-custom-analytics-tracker-tracker {
border: 1px dashed #ccc;
padding: 15px;
background-color: #f9f9f9;
text-align: center;
p {
margin-bottom: 0.5em;
}
}
C. `src/style.scss` – Frontend Styles
Styles that will be applied to the frontend of the website. We will use Shadow DOM here.
/* Styles for the frontend, to be encapsulated in Shadow DOM */
.custom-analytics-tracker-wrapper {
display: block; /* Ensure it takes up space */
border: 1px solid #eee;
padding: 10px;
background-color: #fafafa;
font-family: sans-serif;
color: #333;
p {
margin: 0 0 0.75em 0;
line-height: 1.5;
}
p:last-child {
margin-bottom: 0;
}
.tracker-id-display,
.event-name-display {
font-weight: bold;
color: #0056b3;
}
}
III. Server-Side Rendering and Shadow DOM Encapsulation
For robust style encapsulation, we’ll use PHP to render the block’s HTML and attach it to a Shadow DOM root. This prevents CSS from leaking out or being overridden.
A. Render Callback (`render.php`)
Create a `render.php` file in your plugin’s root directory. This file will contain the PHP code to render the block’s output.
<?php
/**
* Server-side rendering for the Custom Analytics Tracker block.
*
* @package CustomAnalyticsTracker
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Renders the Custom Analytics Tracker block.
*
* @param array $attributes The block attributes.
* @return string The rendered HTML.
*/
function render_custom_analytics_tracker_block( $attributes ) {
$tracking_id = isset( $attributes['trackingId'] ) ? sanitize_text_field( $attributes['trackingId'] ) : '';
$event_name = isset( $attributes['eventName'] ) ? sanitize_text_field( $attributes['eventName'] ) : '';
if ( empty( $tracking_id ) && empty( $event_name ) ) {
return ''; // Don't render if no data is present.
}
// Enqueue the frontend styles specifically for this block's rendering context.
// This ensures styles are loaded only when the block is present.
wp_enqueue_style(
'custom-analytics-tracker-frontend',
plugins_url( 'build/style-index.css', __FILE__ ),
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
);
// The Shadow DOM host element.
$output = '<div class="custom-analytics-tracker-host"></div>';
// JavaScript to attach Shadow DOM and inject content/styles.
// This script will run after the DOM is ready.
ob_start();
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const hostElement = document.querySelector('.custom-analytics-tracker-host');
if (hostElement) {
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
// Inject styles into the Shadow DOM
const style = document.createElement('style');
// Fetch the CSS content. In a real-world scenario, you might embed this
// directly or load it via AJAX if it's very large. For simplicity,
// we'll assume it's available via a global variable or can be fetched.
// A more robust method would involve passing the CSS content via wp_localize_script
// or embedding it directly in the PHP output.
// For this example, we'll use a placeholder and assume the CSS is loaded globally.
// A better approach is to enqueue the CSS and then reference it.
// However, to ensure it's *inside* the shadow DOM, we need to inject it.
// Let's assume style-index.css is enqueued by wp_enqueue_style above.
// We need to get its content. This is a common challenge.
// A practical approach: embed the CSS directly or use wp_add_inline_style.
// Using wp_add_inline_style is the most WordPress-idiomatic way.
// We need to ensure 'custom-analytics-tracker-frontend' is registered and enqueued.
// The PHP code above already enqueues it.
// Now, let's get its content and inject it.
// This is tricky because wp_add_inline_style adds it *after* the enqueued handle.
// A simpler, though less performant for large CSS, method for demonstration:
// Embed the CSS directly.
const cssContent = `
.custom-analytics-tracker-wrapper {
display: block;
border: 1px solid #eee;
padding: 10px;
background-color: #fafafa;
font-family: sans-serif;
color: #333;
}
.custom-analytics-tracker-wrapper p {
margin: 0 0 0.75em 0;
line-height: 1.5;
}
.custom-analytics-tracker-wrapper p:last-child {
margin-bottom: 0;
}
.custom-analytics-tracker-wrapper .tracker-id-display,
.custom-analytics-tracker-wrapper .event-name-display {
font-weight: bold;
color: #0056b3;
}
`;
style.textContent = cssContent;
shadowRoot.appendChild(style);
// Create the content wrapper
const contentWrapper = document.createElement('div');
contentWrapper.className = 'custom-analytics-tracker-wrapper';
// Inject content
if ('') {
const idParagraph = document.createElement('p');
idParagraph.innerHTML = 'Tracking ID: ';
contentWrapper.appendChild(idParagraph);
}
if ('') {
const eventParagraph = document.createElement('p');
eventParagraph.innerHTML = 'Event: ';
contentWrapper.appendChild(eventParagraph);
}
shadowRoot.appendChild(contentWrapper);
}
});
</script>
<?php
$script_content = ob_get_clean();
// Append the script to the output. It will be executed when the page loads.
// Note: This method of embedding script directly can have performance implications
// and might be better handled by wp_localize_script or by enqueuing a separate JS file.
// However, for immediate Shadow DOM attachment, this is a direct approach.
$output .= $script_content;
return $output;
}
// Register the render callback.
// This is done within the block registration hook in custom-analytics-tracker.php,
// but if you were defining the block here, you'd use:
// add_action( 'init', function() {
// register_block_type( 'custom-analytics-tracker/tracker', array(
// 'render_callback' => 'render_custom_analytics_tracker_block',
// // ... other block settings if not using block.json
// ) );
// } );
Explanation:
- The `render_custom_analytics_tracker_block` function receives the block’s attributes.
- It sanitizes the `trackingId` and `eventName`.
- It enqueues the frontend CSS (`build/style-index.css`) using `wp_enqueue_style`. This is crucial for WordPress to know about the stylesheet.
- It outputs a `div` with the class `custom-analytics-tracker-host`. This `div` will serve as the mount point for our Shadow DOM.
- An inline `