• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using Vanilla CSS shadow DOM style layers

Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using Vanilla CSS shadow DOM style layers

Leveraging Shadow DOM for Encapsulated Gutenberg Block Styling

When developing custom Gutenberg blocks, maintaining style isolation is paramount, especially for dynamic components like real-time activity logs. Traditional CSS can lead to style conflicts within the complex WordPress ecosystem. This guide demonstrates how to build a custom Gutenberg block for real-time activity logs, employing Vanilla CSS and the Shadow DOM to achieve robust style encapsulation. We’ll focus on a practical implementation, detailing the necessary PHP, JavaScript, and CSS, along with a clear step-by-step process.

Plugin Structure and Block Registration

First, let’s establish a basic plugin structure. We’ll create a simple plugin that registers our custom block. The block registration process in WordPress involves PHP for server-side registration and JavaScript for client-side rendering and editor interaction.

PHP: Plugin Setup and Block Registration

Create a directory for your plugin, e.g., custom-activity-logs, within wp-content/plugins/. Inside this directory, create the main plugin file, custom-activity-logs.php.

<?php
/**
 * Plugin Name: Custom Activity Logs Block
 * Description: A custom Gutenberg block for displaying real-time activity logs with Shadow DOM styling.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: custom-activity-logs
 */

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

/**
 * Register the custom block.
 */
function custom_activity_logs_register_block() {
    // Automatically load dependencies and version.
    $asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');

    wp_register_script(
        'custom-activity-logs-block-editor-script',
        plugins_url( 'build/index.js', __FILE__ ),
        $asset_file['dependencies'],
        $asset_file['version']
    );

    wp_register_style(
        'custom-activity-logs-block-style',
        plugins_url( 'build/index.css', __FILE__ ),
        array(),
        $asset_file['version']
    );

    register_block_type( 'custom-activity-logs/block', array(
        'editor_script' => 'custom-activity-logs-block-editor-script',
        'editor_style'  => 'custom-activity-logs-block-style',
        'style'         => 'custom-activity-logs-block-style', // Enqueue for frontend too
    ) );
}
add_action( 'init', 'custom_activity_logs_register_block' );

This PHP file registers a script and a style for our block. The index.asset.php file will be generated by our build process and contains dependencies and versioning information. The register_block_type function registers the block with the name custom-activity-logs/block.

JavaScript: Block Editor Implementation

Next, we’ll set up the JavaScript for the block. This involves defining the block’s attributes, its editor representation, and its frontend rendering. We’ll use a build tool like `@wordpress/scripts` for compilation.

Project Setup and Build Process

Navigate to your plugin directory in the terminal and initialize a Node.js project:

cd wp-content/plugins/custom-activity-logs
npm init -y
npm install @wordpress/scripts --save-dev

Add a build script to your package.json:

{
  "name": "custom-activity-logs",
  "version": "1.0.0",
  "description": "",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@wordpress/scripts": "^26.7.0"
  }
}

Now, create the source files in a src/ directory:

mkdir src
touch src/index.js src/editor.scss src/style.scss

JavaScript: Block Definition (src/index.js)

This file defines the block’s metadata, attributes, and the main rendering logic for both the editor and the frontend. We’ll use the render_callback in PHP for frontend rendering to keep the JS focused on the editor experience and initial data fetching.

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import EditorComponent from './editor'; // We'll create this component

// Import styles
import './editor.scss';
import './style.scss';

registerBlockType( 'custom-activity-logs/block', {
    title: __( 'Real-time Activity Log', 'custom-activity-logs' ),
    icon: 'list-view',
    category: 'widgets',
    attributes: {
        logSource: {
            type: 'string',
            default: 'wp_options', // Example: could be a custom post type, user meta, etc.
        },
        maxEntries: {
            type: 'number',
            default: 10,
        },
    },
    edit: ( { attributes, setAttributes } ) => {
        const { logSource, maxEntries } = attributes;

        return (
            <>
                <InspectorControls>
                    <PanelBody title={ __( 'Log Settings', 'custom-activity-logs' ) } initialOpen={ true }>
                        <TextControl
                            label={ __( 'Log Source', 'custom-activity-logs' ) }
                            value={ logSource }
                            onChange={ ( newLogSource ) => setAttributes( { logSource: newLogSource } ) }
                        />
                        <TextControl
                            label={ __( 'Max Entries', 'custom-activity-logs' ) }
                            type="number"
                            value={ maxEntries }
                            onChange={ ( newMaxEntries ) => setAttributes( { maxEntries: parseInt( newMaxEntries, 10 ) } ) }
                        />
                    </PanelBody>
                </InspectorControls>
                <div className="custom-activity-log-editor-wrapper">
                    <h3>{ __( 'Activity Log Preview', 'custom-activity-logs' ) }</h3>
                    <EditorComponent
                        logSource={ logSource }
                        maxEntries={ maxEntries }
                    />
                </div>
            </>
        );
    },
    save: () => {
        // Frontend rendering will be handled by PHP's render_callback
        // This function returns null as we don't need to output static HTML from JS.
        return null;
    },
} );

JavaScript: Editor Component (src/editor.js)

This component will handle the dynamic rendering of the activity log preview within the Gutenberg editor. It will also be responsible for attaching the Shadow DOM.

import React, { useEffect, useRef, useState } from 'react';
import { __ } from '@wordpress/i18n';

const EditorComponent = ( { logSource, maxEntries } ) => {
    const shadowHostRef = useRef( null );
    const [ logEntries, setLogEntries ] = useState( [] );
    const [ shadowRoot, setShadowRoot ] = useState( null );

    // Initialize Shadow DOM
    useEffect( () => {
        if ( shadowHostRef.current && ! shadowHostRef.current.shadowRoot ) {
            const sr = shadowHostRef.current.attachShadow( { mode: 'open' } );
            setShadowRoot( sr );

            // Inject styles into Shadow DOM
            const style = document.createElement( 'style' );
            style.textContent = `
                :host {
                    display: block;
                    border: 1px solid #ccc;
                    padding: 10px;
                    font-family: sans-serif;
                    background-color: #f9f9f9;
                    border-radius: 4px;
                    max-height: 300px;
                    overflow-y: auto;
                }
                .log-entry {
                    margin-bottom: 8px;
                    padding-bottom: 8px;
                    border-bottom: 1px dotted #eee;
                    font-size: 0.9em;
                    color: #333;
                }
                .log-entry:last-child {
                    border-bottom: none;
                    margin-bottom: 0;
                    padding-bottom: 0;
                }
                .log-entry strong {
                    color: #0056b3;
                }
                .log-entry span {
                    color: #666;
                    font-size: 0.85em;
                    margin-left: 10px;
                }
                h4 {
                    margin-top: 0;
                    color: #555;
                    font-size: 1.1em;
                }
            `;
            sr.appendChild( style );
        }
    }, [] );

    // Fetch dummy log data (replace with actual API call if needed)
    useEffect( () => {
        // In a real scenario, you'd fetch this data via WP REST API or AJAX.
        // For this example, we'll simulate data.
        const dummyData = Array.from( { length: maxEntries }, ( _, i ) => ( {
            id: i + 1,
            message: `Activity ${ i + 1 } occurred.`,
            timestamp: new Date( Date.now() - ( maxEntries - i ) * 60000 ).toLocaleString(),
            source: logSource,
        } ) );
        setLogEntries( dummyData );
    }, [ logSource, maxEntries ] );

    // Render content into Shadow DOM
    useEffect( () => {
        if ( shadowRoot ) {
            // Clear previous content
            shadowRoot.innerHTML = '';

            const container = document.createElement( 'div' );
            container.innerHTML = `<h4>${ __( 'Activity Log Preview', 'custom-activity-logs' ) }</h4>`;

            if ( logEntries.length > 0 ) {
                logEntries.forEach( entry => {
                    const entryElement = document.createElement( 'div' );
                    entryElement.className = 'log-entry';
                    entryElement.innerHTML = `
                        ${ entry.message }
                        (${ entry.timestamp }) - Source: ${ entry.source }
                    `;
                    container.appendChild( entryElement );
                } );
            } else {
                const noEntriesElement = document.createElement( 'p' );
                noEntriesElement.textContent = __( 'No log entries found.', 'custom-activity-logs' );
                container.appendChild( noEntriesElement );
            }
            shadowRoot.appendChild( container );
        }
    }, [ logEntries, shadowRoot ] );

    return (
        <div
            ref={ shadowHostRef }
            className="custom-activity-log-shadow-host"
            style={ { minHeight: '100px', border: '1px dashed #aaa', padding: '10px', backgroundColor: '#fff' } } // Placeholder for the shadow host
        >
            { /* Content will be rendered inside the Shadow DOM */ }
        </div>
    );
};

export default EditorComponent;

In src/editor.js:

  • We use useRef to get a reference to the host element where the Shadow DOM will be attached.
  • The first useEffect hook initializes the Shadow DOM and injects the encapsulated CSS. The :host pseudo-class targets the shadow host itself, ensuring styles are scoped.
  • The second useEffect hook simulates fetching log data based on the block’s attributes. In a production environment, this would involve AJAX calls to a custom endpoint or the WordPress REST API.
  • The third useEffect hook is responsible for rendering the actual log entries into the Shadow DOM. It clears previous content and appends new log entries.

CSS: Editor and Frontend Styles (src/editor.scss and src/style.scss)

src/editor.scss will contain styles specific to the editor view, while src/style.scss will contain styles for both the editor and the frontend. Importantly, the styles that need to be encapsulated within the Shadow DOM are injected directly into the Shadow DOM via JavaScript, as shown in src/editor.js. The CSS files here are for the wrapper elements outside the Shadow DOM.

/* src/editor.scss */
.custom-activity-log-editor-wrapper {
    border: 1px solid #e0e0e0;
    padding: 15px;
    background-color: #fefefe;
    border-radius: 5px;
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}

.custom-activity-log-editor-wrapper h3 {
    margin-top: 0;
    color: #444;
    font-size: 1.2em;
    border-bottom: 1px solid #eee;
    padding-bottom: 10px;
    margin-bottom: 15px;
}
/* src/style.scss */
.custom-activity-log-shadow-host {
    /* Styles for the host element itself, outside the shadow DOM.
       These might be minimal as the core styling is inside. */
    margin-bottom: 1em;
}

After creating these files, run the build command:

npm run build

This will generate the build/index.js, build/index.css, and build/index.asset.php files. The index.css will contain the compiled styles from editor.scss and style.scss.

PHP: Frontend Rendering

For the frontend rendering, we’ll use a render_callback in our PHP block registration. This callback will be responsible for outputting the HTML structure that will host the Shadow DOM on the frontend.

Updating Block Registration with render_callback

Modify your custom-activity-logs.php file to include a render_callback.


    <div
        class="custom-activity-log-frontend-host"
        data-log-source=""
        data-max-entries=""
        style="min-height: 100px; border: 1px dashed #aaa; padding: 10px; background-color: #fff;"
    >
        <!-- Content will be rendered inside the Shadow DOM by JavaScript -->
    </div>
     'custom-activity-logs-block-editor-script',
        'editor_style'    => 'custom-activity-logs-block-style',
        'style'           => 'custom-activity-logs-block-style',
        'render_callback' => 'custom_activity_logs_render_block', // Add render_callback
    ) );
}
add_action( 'init', 'custom_activity_logs_register_block' );

The custom_activity_logs_render_block function outputs a simple div with data attributes that will be used by frontend JavaScript to initialize the Shadow DOM and populate it with data. Notice that we are not injecting the Shadow DOM’s internal HTML or CSS directly here. That responsibility is deferred to a dedicated frontend JavaScript file.

Frontend JavaScript for Shadow DOM Initialization

We need a separate JavaScript file to handle the Shadow DOM initialization on the frontend. This script will run after the page loads, find all instances of our block’s host element, and attach the Shadow DOM.

Create a new file src/frontend.js:

document.addEventListener( 'DOMContentLoaded', () => {
    const shadowHosts = document.querySelectorAll( '.custom-activity-log-frontend-host' );

    shadowHosts.forEach( host => {
        if ( host.shadowRoot ) {
            return; // Already initialized
        }

        const shadowRoot = host.attachShadow( { mode: 'open' } );

        // Extract data attributes
        const logSource = host.dataset.logSource || 'wp_options';
        const maxEntries = parseInt( host.dataset.maxEntries, 10 ) || 10;

        // Inject styles into Shadow DOM
        const style = document.createElement( 'style' );
        style.textContent = `
            :host {
                display: block;
                border: 1px solid #ccc;
                padding: 10px;
                font-family: sans-serif;
                background-color: #f9f9f9;
                border-radius: 4px;
                max-height: 300px;
                overflow-y: auto;
            }
            .log-entry {
                margin-bottom: 8px;
                padding-bottom: 8px;
                border-bottom: 1px dotted #eee;
                font-size: 0.9em;
                color: #333;
            }
            .log-entry:last-child {
                border-bottom: none;
                margin-bottom: 0;
                padding-bottom: 0;
            }
            .log-entry strong {
                color: #0056b3;
            }
            .log-entry span {
                color: #666;
                font-size: 0.85em;
                margin-left: 10px;
            }
            h4 {
                margin-top: 0;
                color: #555;
                font-size: 1.1em;
            }
        `;
        shadowRoot.appendChild( style );

        // Fetch and render log data
        const container = document.createElement( 'div' );
        container.innerHTML = `<h4>Activity Log</h4>`; // Simple title for frontend

        // Simulate fetching data
        const dummyData = Array.from( { length: maxEntries }, ( _, i ) => ( {
            id: i + 1,
            message: `Frontend Activity ${ i + 1 } occurred.`,
            timestamp: new Date( Date.now() - ( maxEntries - i ) * 60000 ).toLocaleString(),
            source: logSource,
        } ) );

        if ( dummyData.length > 0 ) {
            dummyData.forEach( entry => {
                const entryElement = document.createElement( 'div' );
                entryElement.className = 'log-entry';
                entryElement.innerHTML = `
                    <strong>${ entry.message }</strong>
                    <span>(${ entry.timestamp }) - Source: ${ entry.source }</span>
                `;
                container.appendChild( entryElement );
            } );
        } else {
            const noEntriesElement = document.createElement( 'p' );
            noEntriesElement.textContent = 'No log entries found.';
            container.appendChild( noEntriesElement );
        }
        shadowRoot.appendChild( container );
    } );
} );

To make this frontend script available, we need to modify our build process. Add frontend.js as an entry point in your @wordpress/scripts configuration. You might need to create a .wp-scripts.json file in your plugin’s root directory:

{
  "entries": [
    "src/index.js",
    "src/frontend.js"
  ]
}

Then, run npm run build again. This will generate build/frontend.js. You’ll also need to enqueue this script. A common way is to add it to the block’s dependencies in PHP, or enqueue it conditionally on the frontend.

Enqueueing Frontend Script

Modify custom-activity-logs.php to enqueue the frontend script. A robust way is to use wp_enqueue_script within a hook that fires on the frontend, ensuring it only loads when needed.





Note: If frontend.js is not a React component and is pure vanilla JS, its dependencies might be simpler (e.g., just `['wp-dom-ready']` or an empty array if it doesn't rely on WordPress internals). The @wordpress/scripts build process will generate an asset file for frontend.js if it's configured as an entry point.

Advanced Considerations and Best Practices

Data Fetching and Real-time Updates

The examples above use dummy data. For a true real-time activity log:

  • WordPress REST API: Create a custom REST API endpoint to fetch log data. Your JavaScript (both editor and frontend) can then use fetch or wp.apiFetch to retrieve this data.
  • WebSockets: For true real-time push updates, integrate a WebSocket solution (e.g., using a plugin like WP-Socket or a custom server). The JavaScript would listen for WebSocket messages and update the Shadow DOM content dynamically without requiring page reloads or polling.
  • AJAX Polling: A simpler, though less efficient, approach is to use setInterval in JavaScript to periodically poll the REST API endpoint for new log entries.

Shadow DOM Styling Layers

The CSS injected directly into the Shadow DOM provides the primary encapsulation. This ensures that styles for the log entries, timestamps, etc., do not leak out and affect other parts of the page, nor do global styles interfere with the log's appearance. The use of :host is crucial for styling the shadow host element itself from within the shadow tree.

Accessibility

Ensure that the content rendered within the Shadow DOM is accessible. Use appropriate ARIA attributes if necessary, especially if the log entries represent interactive elements or status updates. Screen readers can generally access content within Shadow DOM, but proper semantic HTML is key.

Performance

For large log files, consider:

  • Pagination/Lazy Loading: Implement mechanisms to load logs in batches rather than all at once.
  • Debouncing/Throttling: If using real-time updates, debounce or throttle the update logic to prevent excessive DOM manipulations.
  • Efficient Data Fetching: Optimize your API endpoints to return only the necessary data.

Conclusion

By combining Gutenberg's block API with Vanilla CSS and the Shadow DOM, we can create highly encapsulated and maintainable custom components. This approach provides robust style isolation, preventing conflicts and ensuring a consistent user experience for your real-time activity log block, both in the editor and on the frontend.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging and Resolving deep-seated hook priority conflicts in third-party HubSpot Contacts connectors
  • How to securely integrate Salesforce CRM endpoints into WordPress custom plugins using REST API Controllers
  • How to securely integrate Pipedrive custom leads API endpoints into WordPress custom plugins using WP HTTP API
  • Troubleshooting cURL socket timeout limits in production when using modern Genesis child themes wrappers
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Salesforce CRM handlers

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (622)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (829)
  • PHP (5)
  • PHP Development (33)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (601)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (188)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging and Resolving deep-seated hook priority conflicts in third-party HubSpot Contacts connectors
  • How to securely integrate Salesforce CRM endpoints into WordPress custom plugins using REST API Controllers
  • How to securely integrate Pipedrive custom leads API endpoints into WordPress custom plugins using WP HTTP API

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (829)
  • Debugging & Troubleshooting (622)
  • Security & Compliance (601)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala