• 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 automated coupon generator block for Gutenberg using Vanilla CSS shadow DOM style layers

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.

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 Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

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

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

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