Step-by-Step Guide to building a custom automated coupon generator block for Gutenberg using Vanilla CSS shadow DOM style layers
Gutenberg Block Development: Custom Coupon Generator with Shadow DOM Styling
This guide details the construction of a custom Gutenberg block for WordPress, specifically designed to generate and display unique coupon codes. We will leverage Vanilla CSS and the Shadow DOM to encapsulate styles, ensuring no conflicts with the theme or other plugins. This approach is ideal for e-commerce platforms seeking granular control over coupon presentation and functionality.
I. Project Setup and Block Registration
The foundation of any Gutenberg block lies in its registration. We’ll create a simple plugin to house our block. This involves defining the block’s attributes, which will store the coupon’s properties (e.g., code, discount type, value, expiry date).
First, create a new plugin directory, for instance, custom-coupon-generator, within your WordPress wp-content/plugins/ directory. Inside, create a main PHP file, custom-coupon-generator.php.
<?php
/**
* Plugin Name: Custom Coupon Generator
* Description: A custom Gutenberg block for generating and displaying coupons.
* Version: 1.0.0
* Author: Your Name
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register the custom coupon block.
*/
function custom_coupon_generator_register_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_coupon_generator_register_block' );
?>
Next, we need to set up the JavaScript and CSS build process. We’ll use `@wordpress/scripts` for this. Navigate to your plugin directory in your terminal and run:
npm init -y npm install @wordpress/scripts --save-dev
Add a build script to your package.json:
{
"name": "custom-coupon-generator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wordpress/scripts": "^26.10.0"
}
}
Create a src directory within your plugin folder. Inside src, create index.js for your block’s JavaScript entry point and style.scss for its editor styles.
II. Block Definition and Attributes
In src/index.js, we define the block’s metadata and its editable components. We’ll define attributes for the coupon code, discount type, discount value, and expiry date.
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
InspectorControls,
RichText,
} from '@wordpress/block-editor';
import { PanelBody, TextControl, SelectControl, DatePicker } from '@wordpress/components';
import './style.scss';
registerBlockType( 'custom-coupon-generator/coupon-block', {
title: __( 'Coupon Generator', 'custom-coupon-generator' ),
icon: 'tag',
category: 'ecommerce',
attributes: {
couponCode: {
type: 'string',
default: '',
},
discountType: {
type: 'string',
default: 'percentage',
},
discountValue: {
type: 'string',
default: '',
},
expiryDate: {
type: 'string',
default: '',
},
},
edit: EditComponent,
save: SaveComponent,
} );
function EditComponent( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
const { couponCode, discountType, discountValue, expiryDate } = attributes;
const discountTypes = [
{ label: __( 'Percentage', 'custom-coupon-generator' ), value: 'percentage' },
{ label: __( 'Fixed Cart Discount', 'custom-coupon-generator' ), value: 'fixed_cart' },
{ label: __( 'Fixed Product Discount', 'custom-coupon-generator' ), value: 'fixed_product' },
];
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Coupon Settings', 'custom-coupon-generator' ) } initialOpen={ true }>
<TextControl
label={ __( 'Coupon Code', 'custom-coupon-generator' ) }
value={ couponCode }
onChange={ ( newCouponCode ) => setAttributes( { couponCode: newCouponCode } ) }
/>
<SelectControl
label={ __( 'Discount Type', 'custom-coupon-generator' ) }
value={ discountType }
options={ discountTypes }
onChange={ ( newDiscountType ) => setAttributes( { discountType: newDiscountType } ) }
/>
<TextControl
label={ __( 'Discount Value', 'custom-coupon-generator' ) }
type="number"
value={ discountValue }
onChange={ ( newDiscountValue ) => setAttributes( { discountValue: newDiscountValue } ) }
/>
<DatePicker
currentDate={ expiryDate || new Date().toISOString().split('T')[0] }
onChange={ ( newDate ) => setAttributes( { expiryDate: newDate } ) }
is12Hour={ false }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<h3>{ __( 'Coupon Preview', 'custom-coupon-generator' ) }</h3>
<p><strong>{ __( 'Code:', 'custom-coupon-generator' ) }</strong> { couponCode || __( 'Enter coupon code', 'custom-coupon-generator' ) }</p>
<p><strong>{ __( 'Discount:', 'custom-coupon-generator' ) }</strong> { discountValue || '0' } { discountType === 'percentage' ? '%' : '' }</p>
<p><strong>{ __( 'Expires:', 'custom-coupon-generator' ) }</strong> { expiryDate || __( 'Not set', 'custom-coupon-generator' ) }</p>
</div>
</>
);
}
function SaveComponent( { attributes } ) {
const blockProps = useBlockProps.save();
const { couponCode, discountType, discountValue, expiryDate } = attributes;
// In a real-world scenario, you might want to generate the coupon code here
// or fetch it from a backend service if it's dynamic.
// For this example, we're using the user-defined code.
return (
<div { ...blockProps }>
<h3>{ couponCode || __( 'Special Offer', 'custom-coupon-generator' ) }</h3>
<p>{ __( 'Use code', 'custom-coupon-generator' ) } <strong>{ couponCode }</strong> { __( 'for a', 'custom-coupon-generator' ) } { discountValue }{ discountType === 'percentage' ? '%' : '' } { discountType === 'fixed_cart' ? __( 'off your cart', 'custom-coupon-generator' ) : '' } { discountType === 'fixed_product' ? __( 'off a product', 'custom-coupon-generator' ) : '' } { expiryDate ? ` ${ __( 'valid until', 'custom-coupon-generator' ) } ${ expiryDate }` : '' }</p>
</div>
);
}
After creating these files, run the build command in your terminal:
npm run build
This will compile your JavaScript and SCSS into the build directory, which is referenced in our PHP registration function.
III. Implementing Shadow DOM Styling
To achieve style encapsulation, we’ll use the Shadow DOM. This requires a slight modification to how the block is rendered on the frontend. We’ll create a separate JavaScript file for the frontend rendering and enqueue it.
First, update your main plugin file (custom-coupon-generator.php) to enqueue a frontend script:
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Enqueue frontend script for Shadow DOM rendering.
*/
function custom_coupon_generator_enqueue_frontend_script() {
wp_enqueue_script(
'custom-coupon-generator-frontend',
plugins_url( 'build/frontend.js', __FILE__ ),
array( 'wp-element' ), // Depends on React/WP Element
filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
);
// Pass block data to the frontend script if needed, e.g., for dynamic generation.
// For this example, we'll rely on the block's saved HTML.
}
add_action( 'wp_enqueue_scripts', 'custom_coupon_generator_enqueue_frontend_script' );
/**
* Register the custom coupon block.
*/
function custom_coupon_generator_register_block() {
// Register the block using the build directory for both editor and frontend.
// The frontend rendering will be handled by our enqueued script.
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_coupon_generator_register_block' );
Now, create src/frontend.js. This script will find our coupon blocks and attach a Shadow DOM to them.
document.addEventListener( 'DOMContentLoaded', () => {
const couponBlocks = document.querySelectorAll( '.wp-block-custom-coupon-generator-coupon-block' );
couponBlocks.forEach( block => {
// Check if Shadow DOM already exists to prevent duplication
if ( block.shadowRoot ) {
return;
}
const shadowRoot = block.attachShadow( { mode: 'open' } );
// Create a style element for the Shadow DOM
const style = document.createElement( 'style' );
style.textContent = `
:host {
display: block;
border: 1px solid #ccc;
padding: 20px;
border-radius: 8px;
background-color: #f9f9f9;
font-family: sans-serif;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
h3 {
color: #333;
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
p {
margin-bottom: 10px;
color: #555;
}
p strong {
color: #000;
}
.coupon-code {
font-weight: bold;
color: #d9534f; /* A distinct color for the code */
font-size: 1.2em;
letter-spacing: 1px;
}
.expiry-info {
font-style: italic;
color: #777;
font-size: 0.9em;
}
`;
// Move the original content into the Shadow DOM
// We need to clone the children to avoid issues if the block is re-rendered.
const content = block.innerHTML;
block.innerHTML = ''; // Clear original content
shadowRoot.appendChild( style );
const contentWrapper = document.createElement('div');
contentWrapper.innerHTML = content;
shadowRoot.appendChild(contentWrapper);
// Enhance specific elements within the Shadow DOM if needed
const codeElement = shadowRoot.querySelector('p strong');
if (codeElement && codeElement.textContent.includes('Code:')) {
codeElement.parentElement.classList.add('coupon-code');
}
const expiryElement = shadowRoot.querySelector('p:last-child');
if (expiryElement && expiryElement.textContent.includes('Expires:')) {
expiryElement.classList.add('expiry-info');
}
} );
} );
We also need to ensure our block’s saved HTML is structured to be easily targeted. The SaveComponent in src/index.js should produce clean HTML. Let’s refine it:
// ... (previous imports and registerBlockType)
function SaveComponent( { attributes } ) {
const blockProps = useBlockProps.save();
const { couponCode, discountType, discountValue, expiryDate } = attributes;
// Basic validation for display
const displayDiscount = discountValue ? `${discountValue}${discountType === 'percentage' ? '%' : ''}` : '';
const displayOffer = discountType === 'fixed_cart' ? __('off your cart', 'custom-coupon-generator') :
discountType === 'fixed_product' ? __('off a product', 'custom-coupon-generator') :
discountType === 'percentage' ? '%' : '';
return (
<div { ...blockProps }>
<h3>{ couponCode || __( 'Special Offer', 'custom-coupon-generator' ) }</h3>
<p>{ __( 'Use code', 'custom-coupon-generator' ) } <strong class="coupon-code-display">{ couponCode }</strong> { __( 'for a', 'custom-coupon-generator' ) } { displayDiscount } { displayOffer } { expiryDate ? ` ${ __( 'valid until', 'custom-coupon-generator' ) } ${ expiryDate }` : '' }</p>
{ expiryDate && (
<p class="expiry-info">{ __( 'Expires on', 'custom-coupon-generator' ) } { expiryDate }</p>
) }
</div>
);
}
Finally, re-run the build command:
npm run build
Now, when you add the “Coupon Generator” block to a post or page, its content will be rendered within a Shadow DOM. The styles defined in src/frontend.js will be applied exclusively to this Shadow DOM, preventing any CSS bleed-through.
IV. Editor Styles and Refinements
While the Shadow DOM handles frontend encapsulation, the editor view needs its own styling. We’ve already created src/style.scss. Let’s add some basic styles for the editor:
/* src/style.scss */
.wp-block-custom-coupon-generator-coupon-block {
border: 1px dashed #0073aa; /* Distinct dashed border in editor */
padding: 15px;
background-color: #eaf2fa;
border-radius: 5px;
margin-bottom: 1em;
h3 {
color: #005177;
margin-top: 0;
font-size: 1.2em;
}
p {
margin-bottom: 5px;
color: #333;
}
.coupon-code-display,
.expiry-info {
/* Styles for elements that will be targeted by Shadow DOM JS */
/* These might be overridden by Shadow DOM styles, but provide a baseline */
}
}
Ensure your package.json is configured to process this SCSS file. The default `@wordpress/scripts` configuration usually handles this automatically when building.
V. Advanced Considerations and Next Steps
Dynamic Coupon Generation: For true coupon generation, you’d typically integrate with a backend API or WordPress’s own coupon system (if using WooCommerce). The frontend script could be modified to fetch a generated code upon page load or user interaction.
Accessibility: Ensure ARIA attributes are used where appropriate, especially if the coupon code is dynamically revealed or interacted with. The Shadow DOM itself doesn’t inherently break accessibility, but the content within it must be structured correctly.
Internationalization: We’ve used `__()` for translatable strings. Ensure your plugin’s text domain (`custom-coupon-generator`) is correctly set up for translation.
Performance: While Shadow DOM offers style encapsulation, it can introduce a slight overhead. For very high-traffic sites, benchmark performance. The current implementation is lightweight, primarily moving existing DOM content.
Error Handling: Implement checks for missing attributes (e.g., if `couponCode` is empty in the `SaveComponent`) to provide fallback content or warnings.
By following these steps, you can create a robust, self-contained coupon generator Gutenberg block that utilizes Shadow DOM for style isolation, ensuring a clean and predictable presentation across diverse WordPress themes and plugin environments.