Step-by-Step Guide to building a custom real-time audit dashboard block for Gutenberg using Tailwind CSS isolated elements
Setting Up the Development Environment
Before diving into Gutenberg block development, ensure your local WordPress environment is configured for plugin development. This typically involves a local server stack (e.g., LocalWP, Docker with a LEMP/LAMP setup) and Node.js with npm or yarn for managing JavaScript dependencies and build tools.
For this project, we’ll leverage the WordPress `@wordpress/scripts` package, which provides a streamlined build process for JavaScript, CSS, and asset compilation. Initialize your plugin directory and install the necessary development dependencies.
Initializing the Gutenberg Block Plugin
Create a new directory for your plugin within wp-content/plugins/. For example, wp-content/plugins/real-time-audit-dashboard. Inside this directory, create a main plugin file (e.g., real-time-audit-dashboard.php) and a package.json file.
Plugin Header
The main plugin file requires a standard WordPress plugin header.
<?php
/**
* Plugin Name: Real-time Audit Dashboard
* Plugin URI: https://example.com/plugins/real-time-audit-dashboard/
* Description: A custom Gutenberg block for displaying real-time audit data.
* Version: 1.0.0
* Author: Your Name
* Author URI: https://yourwebsite.com/
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: real-time-audit-dashboard
* Domain Path: /languages
*/
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Enqueue block assets.
*/
function rtad_enqueue_block_assets() {
// Enqueue block editor assets.
wp_enqueue_script(
'rtad-editor-script',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-blocks', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-element' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
// Enqueue block editor styles.
wp_enqueue_style(
'rtad-editor-style',
plugins_url( 'build/index.css', __FILE__ ),
array( 'wp-edit-blocks' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
);
// Enqueue frontend styles.
wp_enqueue_style(
'rtad-frontend-style',
plugins_url( 'build/style-index.css', __FILE__ ),
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
);
}
add_action( 'enqueue_block_assets', 'rtad_enqueue_block_assets' );
/**
* Register the block.
*/
function rtad_register_block() {
register_block_type( 'rtad/dashboard', array(
'editor_script' => 'rtad-editor-script',
'editor_style' => 'rtad-editor-style',
'style' => 'rtad-frontend-style',
) );
}
add_action( 'init', 'rtad_register_block' );
package.json for Build Tools
Create a package.json file in the root of your plugin directory to manage dependencies and scripts for the build process.
{
"name": "real-time-audit-dashboard",
"version": "1.0.0",
"description": "A custom Gutenberg block for displaying real-time audit data.",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"packages-update": "wp-scripts packages-update"
},
"keywords": [
"wordpress",
"gutenberg",
"block"
],
"author": "Your Name",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.10.0"
}
}
After creating these files, navigate to your plugin’s root directory in your terminal and run:
npm install
This will install the necessary WordPress development packages. You can then run npm start to begin development (which watches for file changes and recompiles automatically) or npm build to create a production-ready build.
Registering the Block Type
The real-time-audit-dashboard.php file already contains the necessary PHP code to register your block. The register_block_type function is crucial. It takes a block name (namespace/block-name) and an array of arguments, including paths to the editor script and styles, and the frontend style.
Developing the Block’s JavaScript
The core of your Gutenberg block is written in JavaScript. Create a src directory in your plugin’s root. Inside src, create an index.js file. This will be the entry point for your block’s JavaScript.
src/index.js: Block Registration and Editor Component
This file defines the block’s attributes, the editor interface, and the frontend rendering. We’ll use Tailwind CSS for styling, but we’ll ensure its elements are isolated to prevent conflicts with the WordPress admin or other themes/plugins.
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import {
PanelBody,
TextControl,
SelectControl,
Button,
Placeholder,
} from '@wordpress/components';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { useState, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import './style.scss'; // Frontend styles
import './editor.scss'; // Editor-specific styles
// Import Tailwind CSS components (we'll define these later)
import { Card, Table, TableRow, TableCell, Badge } from './tailwind-components';
const blockName = 'rtad/dashboard';
registerBlockType( blockName, {
title: __( 'Real-time Audit Dashboard', 'real-time-audit-dashboard' ),
icon: 'chart-bar', // WordPress Dashicon
category: 'widgets',
description: __( 'Display real-time audit logs and statistics.', 'real-time-audit-dashboard' ),
attributes: {
// Define attributes for block settings, e.g., API endpoint, refresh interval
apiEndpoint: {
type: 'string',
default: '/wp-json/rtad/v1/logs', // Example API endpoint
},
refreshInterval: {
type: 'number',
default: 15, // Seconds
},
},
edit: function( { attributes, setAttributes } ) {
const { apiEndpoint, refreshInterval } = attributes;
const blockProps = useBlockProps();
// State for fetched audit data
const [ auditData, setAuditData ] = useState( null );
const [ isLoading, setIsLoading ] = useState( false );
const [ error, setError ] = useState( null );
// Fetch audit data
const fetchAuditData = async () => {
setIsLoading( true );
setError( null );
try {
const response = await fetch( apiEndpoint );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
const data = await response.json();
setAuditData( data );
} catch ( e ) {
setError( e.message );
console.error( 'Error fetching audit data:', e );
} finally {
setIsLoading( false );
}
};
// Fetch data on mount and set up interval
useEffect( () => {
fetchAuditData(); // Initial fetch
const intervalId = setInterval( fetchAuditData, refreshInterval * 1000 );
// Cleanup interval on unmount
return () => clearInterval( intervalId );
}, [ apiEndpoint, refreshInterval ] ); // Re-fetch if endpoint or interval changes
const handleApiEndpointChange = ( value ) => {
setAttributes( { apiEndpoint: value } );
};
const handleRefreshIntervalChange = ( value ) => {
setAttributes( { refreshInterval: parseInt( value, 10 ) || 15 } );
};
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody title={ __( 'Dashboard Settings', 'real-time-audit-dashboard' ) } initialOpen={ true }>
<TextControl
label={ __( 'API Endpoint', 'real-time-audit-dashboard' ) }
value={ apiEndpoint }
onChange={ handleApiEndpointChange }
help={ __( 'URL to fetch audit data from.', 'real-time-audit-dashboard' ) }
/>
<TextControl
label={ __( 'Refresh Interval (seconds)', 'real-time-audit-dashboard' ) }
type="number"
value={ refreshInterval }
onChange={ handleRefreshIntervalChange }
min="5"
help={ __( 'How often to refresh data in seconds.', 'real-time-audit-dashboard' ) }
/>
</PanelBody>
</InspectorControls>
<Card className="w-full">
<h3 className="text-lg font-semibold mb-4">{ __( 'Audit Log Overview', 'real-time-audit-dashboard' ) }</h3>
{ isLoading && <p>{ __( 'Loading data...', 'real-time-audit-dashboard' ) }</p> }
{ error && <p className="text-red-500">{ __( 'Error: ', 'real-time-audit-dashboard' ) }{ error }</p> }
{ !isLoading && !error && auditData && auditData.length > 0 && (
<Table>
<thead>
<TableRow>
<TableCell>{ __( 'Timestamp', 'real-time-audit-dashboard' ) }</TableCell>
<TableCell>{ __( 'User', 'real-time-audit-dashboard' ) }</TableCell>
<TableCell>{ __( 'Action', 'real-time-audit-dashboard' ) }</TableCell>
<TableCell>{ __( 'Status', 'real-time-audit-dashboard' ) }</TableCell>
</TableRow>
</thead>
<tbody>
{ auditData.map( ( log, index ) => (
<TableRow key={ index }>
<TableCell>{ new Date( log.timestamp ).toLocaleString() }</TableCell>
<TableCell>{ log.user || 'N/A' }</TableCell>
<TableCell>{ log.action }</TableCell>
<TableCell>
<Badge color={ log.status === 'success' ? 'green' : 'red' }>
{ log.status.toUpperCase() }
</Badge>
</TableCell>
</TableRow>
) ) }
</tbody>
</Table>
) }
{ !isLoading && !error && auditData && auditData.length === 0 && (
<p>{ __( 'No audit data available.', 'real-time-audit-dashboard' ) }</p>
) }
{ !isLoading && !error && !auditData && (
<Placeholder
icon="chart-bar"
label={ __( 'Audit Dashboard', 'real-time-audit-dashboard' ) }
instructions={ __( 'Configure the API endpoint in the block settings to fetch audit data.', 'real-time-audit-dashboard' ) }
/>
) }
</Card>
</div>
);
},
save: function( { attributes } ) {
// The frontend rendering will be handled by PHP or a separate JS file if dynamic.
// For simplicity, we'll render a placeholder here and rely on PHP to enqueue frontend scripts.
// A more robust solution might involve a server-side rendering (SSR) approach.
const { apiEndpoint, refreshInterval } = attributes;
const blockProps = useBlockProps.save();
return (
<div { ...blockProps }
data-api-endpoint={ apiEndpoint }
data-refresh-interval={ refreshInterval }>
{ /* Frontend JS will hydrate this */ }
<p>{ __( 'Loading audit data...', 'real-time-audit-dashboard' ) }</p>
</div>
);
},
} );
src/tailwind-components.js: Isolated Tailwind Components
To ensure Tailwind CSS styles don’t leak into the WordPress admin or other parts of the site, we’ll create a dedicated file for our components. These components will use Tailwind classes directly. We’ll then import these into our main index.js.
/**
* Isolated Tailwind CSS Components for Gutenberg Block
*
* These components are designed to be self-contained and use Tailwind classes directly.
* They are intended for use within the Gutenberg editor and on the frontend when
* the block's styles are enqueued.
*/
// Helper function to merge classes, prioritizing blockProps if available
const mergeClasses = ( baseClasses, blockProps = {} ) => {
const { className, ...restProps } = blockProps;
const combinedClasses = `${ baseClasses } ${ className || '' }`.trim();
return { className: combinedClasses, ...restProps };
};
/**
* A simple card component.
* Props: className (for additional Tailwind classes), children
*/
export const Card = ( { className, children } ) => {
const blockProps = useBlockProps.save ? useBlockProps.save( { className } ) : { className };
return (
<div className={ `bg-white shadow-md rounded-lg p-6 mb-4 ${ blockProps.className }` }>
{ children }
</div>
);
};
/**
* A basic table component.
* Props: className, children
*/
export const Table = ( { className, children } ) => {
const blockProps = useBlockProps.save ? useBlockProps.save( { className } ) : { className };
return (
<div className={ `overflow-x-auto ${ blockProps.className }` }>
<table className="min-w-full divide-y divide-gray-200">
{ children }
</table>
</div>
);
};
/**
* A table row component.
* Props: className, children
*/
export const TableRow = ( { className, children } ) => {
const blockProps = useBlockProps.save ? useBlockProps.save( { className } ) : { className };
return (
<tr className={ blockProps.className }>
{ children }
</tr>
);
};
/**
* A table cell component.
* Props: className, children
*/
export const TableCell = ( { className, children } ) => {
const blockProps = useBlockProps.save ? useBlockProps.save( { className } ) : { className };
return (
<td className={ `px-6 py-4 whitespace-nowrap text-sm text-gray-500 ${ blockProps.className }` }>
{ children }
</td>
);
};
/**
* A badge component for status indicators.
* Props: color ('green', 'red'), children
*/
export const Badge = ( { color = 'gray', children } ) => {
const colorClasses = {
green: 'bg-green-100 text-green-800',
red: 'bg-red-100 text-red-800',
gray: 'bg-gray-100 text-gray-800',
};
return (
<span className={ `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ colorClasses[color] }` }>
{ children }
</span>
);
};
// Export all components
export default {
Card,
Table,
TableRow,
TableCell,
Badge,
};
src/editor.scss: Editor Styles
This file is for styles specific to the block editor experience. We’ll import Tailwind’s base styles here and then add any custom editor-only adjustments.
/* Import Tailwind CSS base styles */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom editor styles */
.wp-block[data-type="rtad/dashboard"] {
/* Add any specific styles for the block in the editor */
border: 1px dashed #ccc;
padding: 1rem;
background-color: #f9f9f9;
}
/* Ensure Tailwind components are isolated */
/* No global Tailwind styles should be applied here that affect the WP admin */
src/style.scss: Frontend Styles
This SCSS file will be compiled into build/style-index.css and enqueued on both the frontend and the editor. It should contain the core styles for your block.
/* Import Tailwind CSS base styles */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Frontend styles for the dashboard block */
.wp-block-rtad-dashboard {
/* Styles applied to the block wrapper on the frontend */
/* These will be overridden by the blockProps className if provided */
}
/* Ensure Tailwind components are isolated */
/* No global Tailwind styles should be applied here that affect the WP admin */
Integrating Tailwind CSS
The @wordpress/scripts package can be configured to process Tailwind CSS. You’ll need a tailwind.config.js file in your plugin’s root directory.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.js', // Scan JS files for Tailwind classes
'./*.php', // Scan PHP files for Tailwind classes (less common but possible)
'./**/*.php', // Scan all PHP files
],
theme: {
extend: {},
},
plugins: [],
// Important: Prefix all generated classes to avoid conflicts
prefix: 'tw-',
// Safelist classes to ensure they are not purged if not directly found in content
safelist: [
'bg-green-100', 'text-green-800',
'bg-red-100', 'text-red-800',
'bg-gray-100', 'text-gray-800',
'text-lg', 'font-semibold', 'mb-4',
'w-full', 'shadow-md', 'rounded-lg', 'p-6', 'mb-4',
'overflow-x-auto', 'min-w-full', 'divide-y', 'divide-gray-200',
'px-6', 'py-4', 'whitespace-nowrap', 'text-sm', 'text-gray-500',
'inline-flex', 'items-center', 'px-2.5', 'py-0.5', 'rounded-full', 'text-xs', 'font-medium',
'text-red-500',
// Add any other classes used in your components
],
};
The prefix: 'tw-' option is critical for isolating Tailwind. This means all Tailwind classes will be prefixed with tw- (e.g., text-red-500 becomes tw-text-red-500). You’ll need to adjust your tailwind.config.js and your SCSS imports accordingly.
Update your src/editor.scss and src/style.scss to use the prefixed classes:
/* src/editor.scss */
@tailwind base;
@tailwind components;
@tailwind utilities;
.wp-block[data-type="rtad/dashboard"] {
border: 1px dashed #ccc;
padding: 1rem;
background-color: #f9f9f9;
}
/* Example of prefixed classes */
.tw-bg-white { background-color: #fff; }
.tw-shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
/* ... and so on for all Tailwind classes used */
/* src/style.scss */
@tailwind base;
@tailwind components;
@tailwind utilities;
.wp-block-rtad-dashboard {
/* Frontend styles */
}
/* Example of prefixed classes */
.tw-bg-white { background-color: #fff; }
.tw-shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
/* ... and so on for all Tailwind classes used */
And update your tailwind.config.js to use the prefix:
/** @type {import('tailwindcss').Config} */
module.exports = {
// ... other configurations
prefix: 'tw-', // This is the key setting
// ... safelist
};
After making these changes, run npm start or npm build. The build process will compile your SCSS files, including the Tailwind directives, into CSS files in the build directory. The prefix ensures that these styles are applied only within the context of your block.
Frontend Rendering and Data Fetching
The save function in src/index.js renders a placeholder element. For dynamic content like a real-time dashboard, you have a few options:
- Client-side Hydration: The
savefunction outputs data attributes (likedata-api-endpoint) that a separate frontend JavaScript file can read to initialize the dashboard. This is the approach hinted at in thesavefunction. - Server-Side Rendering (SSR): The block’s PHP registration can include a
render_callbackfunction that dynamically generates the HTML on the server. This is often more performant but can be more complex for real-time updates. - Dynamic Blocks with REST API: A hybrid approach where the block is registered as dynamic, and its
editfunction handles the editor view, while thesavefunction outputs a placeholder that a frontend script (enqueued via PHP) then populates by fetching data from a custom REST API endpoint.
For this example, we’ll assume a client-side hydration approach. The save function outputs the necessary data attributes. You would then enqueue a separate JavaScript file on the frontend that targets these elements and fetches/updates the data.
Example Frontend JavaScript (src/frontend.js)
Create a new file, src/frontend.js, and add the following code. You’ll need to modify your real-time-audit-dashboard.php to enqueue this script on the frontend.
document.addEventListener( 'DOMContentLoaded', () => {
const dashboardBlocks = document.querySelectorAll( '.wp-block-rtad-dashboard' );
dashboardBlocks.forEach( ( block ) => {
const apiEndpoint = block.dataset.apiEndpoint;
const refreshInterval = parseInt( block.dataset.refreshInterval, 10 ) || 15; // Default to 15 seconds
if ( ! apiEndpoint ) {
console.warn( 'RTAD Dashboard: API endpoint not configured for this block.' );
return;
}
const renderTable = ( data ) => {
if ( ! data || data.length === 0 ) {
block.innerHTML = '<p>No audit data available.</p>';
return;
}
let tableHtml = `
<div class="bg-white shadow-md rounded-lg p-6 mb-4">
<h3 class="text-lg font-semibold mb-4">Audit Log Overview</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Timestamp</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
`;
data.forEach( ( log ) => {
const timestamp = new Date( log.timestamp ).toLocaleString();
const user = log.user || 'N/A';
const action = log.action;
const status = log.status.toUpperCase();
const badgeColorClass = status === 'SUCCESS' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
tableHtml += `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${ timestamp }</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${ user }</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${ action }</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ badgeColorClass }">
${ status }
</span>
</td>
</tr>
`;
} );
tableHtml += `
</tbody>
</table>
</div>
</div>
`;
block.innerHTML = tableHtml;
};
const fetchAndRender = async () => {
try {
const response = await fetch( apiEndpoint );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
const data = await response.json();
renderTable( data );
} catch ( e ) {
console.error( 'RTAD Frontend Error:', e );
block.innerHTML = `<p class="text-red-500">Error loading audit data: ${ e.message }</p>`;
}
};
fetchAndRender(); // Initial fetch
const intervalId = setInterval( fetchAndRender, refreshInterval * 1000 );
// Cleanup interval on block removal or page unload
return () => clearInterval( intervalId );
} );
} );
Enqueueing Frontend Scripts
Modify your main plugin file (real-time-audit-dashboard.php) to enqueue frontend.js and its associated styles. You’ll also need to ensure Tailwind CSS is processed for the frontend.