• 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 dynamic lead collector block for Gutenberg using Tailwind CSS isolated elements

Step-by-Step Guide to building a custom dynamic lead collector block for Gutenberg using Tailwind CSS isolated elements

npm init -y
npm install @wordpress/scripts --save-dev

Add a build script to your package.json:

{
    // ... other package.json content
    "scripts": {
        "build": "wp-scripts build",
        "start": "wp-scripts start"
    }
    // ...
}

Finally, create a build directory and an empty index.js and index.css file within it. These will be populated by the build process.

mkdir build
touch build/index.js
touch build/index.css
touch build/style-index.css

Run the build command to ensure everything is set up correctly. This will generate the necessary files in the build directory.

npm run build

Frontend and Editor JavaScript Implementation

The core of our dynamic block lies in its JavaScript. We’ll use the `@wordpress/blocks` and `@wordpress/element` packages to define the block’s behavior in the editor and on the frontend. Our JavaScript will handle rendering the form, managing input states, and potentially submitting data.

Edit build/index.js to define the block’s editor and frontend rendering functions. We’ll use React components for this.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, ColorPicker, RangeControl } from '@wordpress/components';
import { useState } from '@wordpress/element';

// Import CSS
import './index.css';
import './style-index.css';

const Edit = ( { attributes, setAttributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    const blockProps = useBlockProps( {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
    } );

    const onChangeTitle = ( newTitle ) => {
        setAttributes( { titleText: newTitle } );
    };

    const onChangeButtonText = ( newButtonText ) => {
        setAttributes( { buttonText: newButtonText } );
    };

    const onChangeBackgroundColor = ( newColor ) => {
        setAttributes( { backgroundColor: newColor.hex } );
    };

    const onChangeTextColor = ( newColor ) => {
        setAttributes( { textColor: newColor.hex } );
    };

    const onChangePadding = ( value, side ) => {
        setAttributes( {
            padding: {
                ...padding,
                [ side ]: `${ value }px`,
            },
        } );
    };

    // In the editor, we render a placeholder form structure.
    // Actual form submission logic would typically be handled server-side or via AJAX.
    return (
        <>
            <InspectorControls>
                <PanelBody title="Lead Collector Settings" initialOpen={ true }>
                    <TextControl
                        label="Title Text"
                        value={ titleText }
                        onChange={ onChangeTitle }
                    />
                    <TextControl
                        label="Button Text"
                        value={ buttonText }
                        onChange={ onChangeButtonText }
                    />
                    <p>Background Color</p>
                    <ColorPicker
                        color={ backgroundColor }
                        onChangeComplete={ onChangeBackgroundColor }
                        disableAlpha
                    />
                    <p>Text Color</p>
                    <ColorPicker
                        color={ textColor }
                        onChangeComplete={ onChangeTextColor }
                        disableAlpha
                    />
                    <RangeControl
                        label="Padding Top (px)"
                        value={ parseInt( padding.top ) }
                        onChange={ ( value ) => onChangePadding( value, 'top' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Bottom (px)"
                        value={ parseInt( padding.bottom ) }
                        onChange={ ( value ) => onChangePadding( value, 'bottom' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                     <RangeControl
                        label="Padding Left (px)"
                        value={ parseInt( padding.left ) }
                        onChange={ ( value ) => onChangePadding( value, 'left' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Right (px)"
                        value={ parseInt( padding.right ) }
                        onChange={ ( value ) => onChangePadding( value, 'right' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <RichText
                    tagName="h3"
                    value={ titleText }
                    onChange={ onChangeTitle }
                    placeholder="Enter title..."
                    className="lead-collector-title"
                />
                <form className="lead-collector-form">
                    <div className="lead-collector-input-group">
                        <label htmlFor="lead-name">Name:</label>
                        <input type="text" id="lead-name" name="lead_name" placeholder="Your Name" />
                    </div>
                    <div className="lead-collector-input-group">
                        <label htmlFor="lead-email">Email:</label>
                        <input type="email" id="lead-email" name="lead_email" placeholder="Your Email" />
                    </div>
                    <button type="submit" className="lead-collector-submit-button">
                        { buttonText }
                    </button>
                </form>
            </div>
        </>
    );
};

const Save = ( { attributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    // For the frontend, we apply styles directly.
    // In a production scenario, you might want to generate CSS classes dynamically
    // or enqueue a separate stylesheet based on attributes.
    const blockProps = {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
        className: 'wp-block-custom-lead-collector-lead-form', // Standard WordPress block class
    };

    // The 'Save' function determines the HTML that is saved to the database.
    // It should not contain interactive editor components.
    return (
        <div { ...blockProps }>
            <h3 className="lead-collector-title">{ titleText }</h3>
            <form className="lead-collector-form">
                <div className="lead-collector-input-group">
                    <label htmlFor="lead-name-frontend">Name:</label>
                    <input type="text" id="lead-name-frontend" name="lead_name" placeholder="Your Name" />
                </div>
                <div className="lead-collector-input-group">
                    <label htmlFor="lead-email-frontend">Email:</label>
                    <input type="email" id="lead-email-frontend" name="lead_email" placeholder="Your Email" />
                </div>
                <button type="submit" className="lead-collector-submit-button">
                    { buttonText }
                </button>
            </form>
        </div>
    );
};

registerBlockType( 'custom-lead-collector/lead-form', {
    edit: Edit,
    save: Save,
} );

The Edit component handles the block’s appearance and controls within the Gutenberg editor. It uses useBlockProps to apply necessary wrapper attributes and inline styles derived from block attributes. InspectorControls provides a sidebar panel for users to customize the block’s title, button text, colors, and padding.

The Save component defines the static HTML that will be rendered on the frontend. It mirrors the structure of the editor view but without the interactive controls. Styles are applied inline for simplicity, but for more complex styling, consider generating CSS classes.

Styling with Tailwind CSS Isolated Elements

To achieve dynamic and isolated styling, we’ll leverage Tailwind CSS utility classes. For this example, we’ll assume Tailwind CSS is already set up in your WordPress theme or via a separate build process. We’ll apply utility classes directly within our JSX for the editor and frontend.

First, ensure your theme or build process compiles Tailwind CSS. If you’re using `@wordpress/scripts`, you can configure it to process Tailwind. For this demonstration, we’ll focus on applying the classes.

Modify the build/index.css file to include any base styles or custom CSS that isn’t directly handled by Tailwind utilities. This file is enqueued for the editor.

/* build/index.css */
.wp-block-custom-lead-collector-lead-form {
    @apply border border-gray-300 rounded-lg shadow-sm;
}

.lead-collector-title {
    @apply text-2xl font-bold mb-4;
}

.lead-collector-form {
    @apply space-y-4;
}

.lead-collector-input-group {
    @apply flex flex-col;
}

.lead-collector-input-group label {
    @apply text-sm font-medium text-gray-700;
}

.lead-collector-input-group input[type="text"],
.lead-collector-input-group input[type="email"] {
    @apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm;
}

.lead-collector-submit-button {
    @apply w-full inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
}

The build/style-index.css file is enqueued for both the editor and the frontend. It should contain styles that are common to both environments.

/* build/style-index.css */
/* Global styles for the block */
.wp-block-custom-lead-collector-lead-form {
    /* Example: Ensure consistent box-sizing */
    box-sizing: border-box;
}

/* Specific styles for elements within the block */
.lead-collector-title {
    /* Example: Ensure title is responsive */
    word-break: break-word;
}

.lead-collector-form .lead-collector-input-group input {
    /* Example: Ensure inputs have a minimum height */
    min-height: 38px;
}

.lead-collector-submit-button {
    /* Example: Ensure button has a minimum height */
    min-height: 40px;
}

Now, let’s integrate these Tailwind classes into our JavaScript components. We’ll apply them directly to the JSX elements.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, ColorPicker, RangeControl } from '@wordpress/components';
import { useState } from '@wordpress/element';

// Import CSS
import './index.css';
import './style-index.css';

const Edit = ( { attributes, setAttributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    const blockProps = useBlockProps( {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
        className: 'wp-block-custom-lead-collector-lead-form', // Apply base block class
    } );

    const onChangeTitle = ( newTitle ) => {
        setAttributes( { titleText: newTitle } );
    };

    const onChangeButtonText = ( newButtonText ) => {
        setAttributes( { buttonText: newButtonText } );
    };

    const onChangeBackgroundColor = ( newColor ) => {
        setAttributes( { backgroundColor: newColor.hex } );
    };

    const onChangeTextColor = ( newColor ) => {
        setAttributes( { textColor: newColor.hex } );
    };

    const onChangePadding = ( value, side ) => {
        setAttributes( {
            padding: {
                ...padding,
                [ side ]: `${ value }px`,
            },
        } );
    };

    return (
        <>
            <InspectorControls>
                <PanelBody title="Lead Collector Settings" initialOpen={ true }>
                    <TextControl
                        label="Title Text"
                        value={ titleText }
                        onChange={ onChangeTitle }
                    />
                    <TextControl
                        label="Button Text"
                        value={ buttonText }
                        onChange={ onChangeButtonText }
                    />
                    <p>Background Color</p>
                    <ColorPicker
                        color={ backgroundColor }
                        onChangeComplete={ onChangeBackgroundColor }
                        disableAlpha
                    />
                    <p>Text Color</p>
                    <ColorPicker
                        color={ textColor }
                        onChangeComplete={ onChangeTextColor }
                        disableAlpha
                    />
                    <RangeControl
                        label="Padding Top (px)"
                        value={ parseInt( padding.top ) }
                        onChange={ ( value ) => onChangePadding( value, 'top' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Bottom (px)"
                        value={ parseInt( padding.bottom ) }
                        onChange={ ( value ) => onChangePadding( value, 'bottom' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                     <RangeControl
                        label="Padding Left (px)"
                        value={ parseInt( padding.left ) }
                        onChange={ ( value ) => onChangePadding( value, 'left' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Right (px)"
                        value={ parseInt( padding.right ) }
                        onChange={ ( value ) => onChangePadding( value, 'right' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <RichText
                    tagName="h3"
                    value={ titleText }
                    onChange={ onChangeTitle }
                    placeholder="Enter title..."
                    className="lead-collector-title" // Tailwind class
                />
                <form className="lead-collector-form"> {/* Tailwind classes */}
                    <div className="lead-collector-input-group"> {/* Tailwind classes */}
                        <label htmlFor="lead-name">Name:</label>
                        <input type="text" id="lead-name" name="lead_name" placeholder="Your Name" />
                    </div>
                    <div className="lead-collector-input-group"> {/* Tailwind classes */}
                        <label htmlFor="lead-email">Email:</label>
                        <input type="email" id="lead-email" name="lead_email" placeholder="Your Email" />
                    </div>
                    <button type="submit" className="lead-collector-submit-button"> {/* Tailwind class */}
                        { buttonText }
                    </button>
                </form>
            </div>
        </>
    );
};

const Save = ( { attributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    const blockProps = {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
        className: 'wp-block-custom-lead-collector-lead-form', // Standard WordPress block class
    };

    return (
        <div { ...blockProps }>
            <h3 className="lead-collector-title">{ titleText }</h3> {/* Tailwind class */}
            <form className="lead-collector-form"> {/* Tailwind classes */}
                <div className="lead-collector-input-group"> {/* Tailwind classes */}
                    <label htmlFor="lead-name-frontend">Name:</label>
                    <input type="text" id="lead-name-frontend" name="lead_name" placeholder="Your Name" />
                </div>
                <div className="lead-collector-input-group"> {/* Tailwind classes */}
                    <label htmlFor="lead-email-frontend">Email:</label>
                    <input type="email" id="lead-email-frontend" name="lead_email" placeholder="Your Email" />
                </div>
                <button type="submit" className="lead-collector-submit-button"> {/* Tailwind class */}
                    { buttonText }
                </button>
            </form>
        </div>
    );
};

registerBlockType( 'custom-lead-collector/lead-form', {
    edit: Edit,
    save: Save,
} );

The key here is that the Tailwind classes are applied directly to the elements. When the CSS is compiled, these classes will resolve to specific CSS properties. This approach ensures that the styling is encapsulated within the block’s generated CSS files (index.css and style-index.css), preventing conflicts with other site styles.

Dynamic Data Handling and Submission

For a lead collector, the primary dynamic aspect is data submission. While Gutenberg blocks primarily focus on presentation and editor interaction, actual data handling requires server-side logic. We’ll outline a common approach using AJAX.

First, we need to register a REST API endpoint in WordPress to receive the form data. Add this to your main plugin file (custom-lead-collector.php).

<?php
/**
 * ... (plugin header) ...
 */

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

// ... (block registration function) ...

/**
 * Registers a REST API endpoint for lead submission.
 */
function custom_lead_collector_register_api_route() {
    register_rest_route( 'custom-lead-collector/v1', '/submit', array(
        'methods'  => WP_REST_Server::CREATABLE, // Accepts POST requests
        'callback' => 'custom_lead_collector_handle_submission',
        'permission_callback' => '__return_true', // For simplicity, allow anyone. In production, implement proper permissions.
    ) );
}
add_action( 'rest_api_init', 'custom_lead_collector_register_api_route' );

/**
 * Handles the lead submission data.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_REST_Response Response object.
 */
function custom_lead_collector_handle_submission( WP_REST_Request $request ) {
    $name  = sanitize_text_field( $request->get_param( 'name' ) );
    $email = sanitize_email( $request->get_param( 'email' ) );

    if ( empty( $name ) || ! is_email( $email ) ) {
        return new WP_REST_Response( array( 'message' => 'Invalid data provided.' ), 400 );
    }

    // Here you would typically:
    // 1. Save the lead to a custom database table.
    // 2. Send an email notification to the site administrator.
    // 3. Send a confirmation email to the user (optional).
    // 4. Log the submission.

    // Example: Saving to post meta for a custom post type 'leads' (requires CPT registration)
    // $post_data = array(
    //     'post_title'    => sanitize_text_field( $name ) . ' - ' . $email,
    //     'post_status'   => 'publish',
    //     'post_type'     => 'lead', // Assuming 'lead' is a registered custom post type
    //     'meta_input'    => array(
    //         '_lead_name'  => $name,
    //         '_lead_email' => $email,
    //     ),
    // );
    // $post_id = wp_insert_post( $post_data );

    // For demonstration, we'll just return a success message.
    return new WP_REST_Response( array( 'message' => 'Lead submitted successfully!' ), 200 );
}
?>

Next, we need to add JavaScript to handle the form submission from the frontend. This JavaScript will intercept the form’s default submission, send the data to our REST API endpoint using `fetch`, and provide user feedback.

Add this JavaScript to your build/index.js file, typically within a separate function that runs on the frontend.

// ... (previous imports and Edit/Save components) ...

// Frontend script to handle form submission
function handleLeadFormSubmission() {
    const forms = document.querySelectorAll( '.wp-block-custom-lead-collector-lead-form .lead-collector-form' );

    forms.forEach( form => {
        form.addEventListener( 'submit', async ( event ) => {
            event.preventDefault(); // Prevent default form submission

            const formData = new FormData( form );
            const nameInput = form.querySelector( 'input[name="lead_name"]' );
            const emailInput = form.querySelector( 'input[name="lead_email"]' );

            const data = {
                name: nameInput.value,
                email: emailInput.value,
            };

            // Basic client-side validation
            if ( ! data.name || ! data.email || ! data.email.includes('@') ) {
                alert( 'Please enter a valid name and email address.' );
                return;
            }

            try {
                const response = await fetch( '/wp-json/custom-lead-collector/v1/submit', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': customLeadCollectorSettings.nonce, // If nonce is needed for authentication
                    },
                    body: JSON.stringify( data ),
                } );

                const result = await response.json();

                if ( response.ok ) {
                    alert( result.message || 'Thank you for your submission!' );
                    form.reset(); // Clear the form
                } else {
                    alert( result.message || 'An error occurred. Please try again later.' );
                }
            } catch ( error ) {
                console.error( 'Submission error:', error );
                alert( 'An unexpected error occurred. Please try again later.' );
            }
        } );
    } );
}

// Enqueue the frontend script
// This requires a wp_enqueue_script call in your PHP, typically in custom-lead-collector.php
// or a separate frontend assets file.
// For simplicity, we'll assume it's handled.
// To make this work, you'd need to pass the nonce to the frontend.
// Example PHP:
// wp_enqueue_script(
//     'custom-lead-collector-frontend',
//     plugins_url( 'build/frontend.js', __FILE__ ),
//     array( 'wp-element', 'wp-api-fetch' ), // wp-api-fetch is useful for nonce handling
//     filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
// );
// wp_localize_script( 'custom-lead-collector-frontend', 'customLeadCollectorSettings', array(
//     'nonce' => wp_create_nonce( 'wp_rest' ),
// ) );

// Call the submission handler when the DOM is ready
document.addEventListener( 'DOMContentLoaded', handleLeadFormSubmission );

// Re-register the block type with the same name
registerBlockType( 'custom-lead-collector/lead-form', {
    edit: Edit,
    save: Save,
} );

To make the frontend JavaScript work correctly, you need to enqueue it and localize necessary data (like nonces for security) using `wp_enqueue_script` and `wp_localize_script

Plugin Scaffolding and Core Registration

We’ll begin by establishing the foundational structure for our custom Gutenberg block plugin. This involves creating the necessary directory structure and registering the block with WordPress. For this example, we’ll assume a plugin named custom-lead-collector.

First, create the plugin directory within your WordPress installation’s wp-content/plugins/ directory:

mkdir wp-content/plugins/custom-lead-collector
cd wp-content/plugins/custom-lead-collector
touch custom-lead-collector.php
mkdir src
touch src/init.php

Next, populate the main plugin file (custom-lead-collector.php) with the standard plugin header and the block registration logic. We’ll use the register_block_type function, pointing to our block’s metadata file.

<?php
/**
 * Plugin Name: Custom Lead Collector Block
 * Description: A custom Gutenberg block for collecting leads with dynamic styling.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: custom-lead-collector
 *
 * @package CustomLeadCollector
 */

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_lead_collector_block_init() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_lead_collector_block_init' );
?>

Now, let’s set up the core block registration in src/init.php. This file will contain the logic for defining the block’s attributes, editor script, and frontend script.

<?php
/**
 * File: src/init.php
 *
 * Core block registration and asset enqueuing.
 */

// This file is intentionally left blank for this example,
// as the primary registration is handled by block.json and the main plugin file.
// In more complex scenarios, this file would contain attribute definitions,
// script/style enqueuing logic if not managed by block.json, etc.
?>

For modern Gutenberg development, we leverage block.json to define block metadata, attributes, and asset dependencies. Create this file at the root of your plugin directory.

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "custom-lead-collector/lead-form",
    "version": "0.1.0",
    "title": "Custom Lead Collector",
    "category": "widgets",
    "icon": "email-alt",
    "description": "A dynamic block for collecting user leads.",
    "keywords": [ "lead", "form", "collector", "contact" ],
    "attributes": {
        "titleText": {
            "type": "string",
            "default": "Get in Touch!"
        },
        "buttonText": {
            "type": "string",
            "default": "Submit"
        },
        "backgroundColor": {
            "type": "string",
            "default": "#f0f0f0"
        },
        "textColor": {
            "type": "string",
            "default": "#333333"
        },
        "padding": {
            "type": "object",
            "default": {
                "top": "20px",
                "bottom": "20px",
                "left": "20px",
                "right": "20px"
            }
        }
    },
    "textdomain": "custom-lead-collector",
    "editorScript": "file:./build/index.js",
    "editorStyle": "file:./build/index.css",
    "style": "file:./build/style-index.css",
    "supports": {
        "html": false,
        "align": [ "wide", "full" ]
    }
}

To manage the build process for our JavaScript and CSS, we’ll use npm and a build tool like `@wordpress/scripts`. Initialize your project with npm:

npm init -y
npm install @wordpress/scripts --save-dev

Add a build script to your package.json:

{
    // ... other package.json content
    "scripts": {
        "build": "wp-scripts build",
        "start": "wp-scripts start"
    }
    // ...
}

Finally, create a build directory and an empty index.js and index.css file within it. These will be populated by the build process.

mkdir build
touch build/index.js
touch build/index.css
touch build/style-index.css

Run the build command to ensure everything is set up correctly. This will generate the necessary files in the build directory.

npm run build

Frontend and Editor JavaScript Implementation

The core of our dynamic block lies in its JavaScript. We’ll use the `@wordpress/blocks` and `@wordpress/element` packages to define the block’s behavior in the editor and on the frontend. Our JavaScript will handle rendering the form, managing input states, and potentially submitting data.

Edit build/index.js to define the block’s editor and frontend rendering functions. We’ll use React components for this.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, ColorPicker, RangeControl } from '@wordpress/components';
import { useState } from '@wordpress/element';

// Import CSS
import './index.css';
import './style-index.css';

const Edit = ( { attributes, setAttributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    const blockProps = useBlockProps( {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
    } );

    const onChangeTitle = ( newTitle ) => {
        setAttributes( { titleText: newTitle } );
    };

    const onChangeButtonText = ( newButtonText ) => {
        setAttributes( { buttonText: newButtonText } );
    };

    const onChangeBackgroundColor = ( newColor ) => {
        setAttributes( { backgroundColor: newColor.hex } );
    };

    const onChangeTextColor = ( newColor ) => {
        setAttributes( { textColor: newColor.hex } );
    };

    const onChangePadding = ( value, side ) => {
        setAttributes( {
            padding: {
                ...padding,
                [ side ]: `${ value }px`,
            },
        } );
    };

    // In the editor, we render a placeholder form structure.
    // Actual form submission logic would typically be handled server-side or via AJAX.
    return (
        <>
            <InspectorControls>
                <PanelBody title="Lead Collector Settings" initialOpen={ true }>
                    <TextControl
                        label="Title Text"
                        value={ titleText }
                        onChange={ onChangeTitle }
                    />
                    <TextControl
                        label="Button Text"
                        value={ buttonText }
                        onChange={ onChangeButtonText }
                    />
                    <p>Background Color</p>
                    <ColorPicker
                        color={ backgroundColor }
                        onChangeComplete={ onChangeBackgroundColor }
                        disableAlpha
                    />
                    <p>Text Color</p>
                    <ColorPicker
                        color={ textColor }
                        onChangeComplete={ onChangeTextColor }
                        disableAlpha
                    />
                    <RangeControl
                        label="Padding Top (px)"
                        value={ parseInt( padding.top ) }
                        onChange={ ( value ) => onChangePadding( value, 'top' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Bottom (px)"
                        value={ parseInt( padding.bottom ) }
                        onChange={ ( value ) => onChangePadding( value, 'bottom' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                     <RangeControl
                        label="Padding Left (px)"
                        value={ parseInt( padding.left ) }
                        onChange={ ( value ) => onChangePadding( value, 'left' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Right (px)"
                        value={ parseInt( padding.right ) }
                        onChange={ ( value ) => onChangePadding( value, 'right' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <RichText
                    tagName="h3"
                    value={ titleText }
                    onChange={ onChangeTitle }
                    placeholder="Enter title..."
                    className="lead-collector-title"
                />
                <form className="lead-collector-form">
                    <div className="lead-collector-input-group">
                        <label htmlFor="lead-name">Name:</label>
                        <input type="text" id="lead-name" name="lead_name" placeholder="Your Name" />
                    </div>
                    <div className="lead-collector-input-group">
                        <label htmlFor="lead-email">Email:</label>
                        <input type="email" id="lead-email" name="lead_email" placeholder="Your Email" />
                    </div>
                    <button type="submit" className="lead-collector-submit-button">
                        { buttonText }
                    </button>
                </form>
            </div>
        </>
    );
};

const Save = ( { attributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    // For the frontend, we apply styles directly.
    // In a production scenario, you might want to generate CSS classes dynamically
    // or enqueue a separate stylesheet based on attributes.
    const blockProps = {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
        className: 'wp-block-custom-lead-collector-lead-form', // Standard WordPress block class
    };

    // The 'Save' function determines the HTML that is saved to the database.
    // It should not contain interactive editor components.
    return (
        <div { ...blockProps }>
            <h3 className="lead-collector-title">{ titleText }</h3>
            <form className="lead-collector-form">
                <div className="lead-collector-input-group">
                    <label htmlFor="lead-name-frontend">Name:</label>
                    <input type="text" id="lead-name-frontend" name="lead_name" placeholder="Your Name" />
                </div>
                <div className="lead-collector-input-group">
                    <label htmlFor="lead-email-frontend">Email:</label>
                    <input type="email" id="lead-email-frontend" name="lead_email" placeholder="Your Email" />
                </div>
                <button type="submit" className="lead-collector-submit-button">
                    { buttonText }
                </button>
            </form>
        </div>
    );
};

registerBlockType( 'custom-lead-collector/lead-form', {
    edit: Edit,
    save: Save,
} );

The Edit component handles the block’s appearance and controls within the Gutenberg editor. It uses useBlockProps to apply necessary wrapper attributes and inline styles derived from block attributes. InspectorControls provides a sidebar panel for users to customize the block’s title, button text, colors, and padding.

The Save component defines the static HTML that will be rendered on the frontend. It mirrors the structure of the editor view but without the interactive controls. Styles are applied inline for simplicity, but for more complex styling, consider generating CSS classes.

Styling with Tailwind CSS Isolated Elements

To achieve dynamic and isolated styling, we’ll leverage Tailwind CSS utility classes. For this example, we’ll assume Tailwind CSS is already set up in your WordPress theme or via a separate build process. We’ll apply utility classes directly within our JSX for the editor and frontend.

First, ensure your theme or build process compiles Tailwind CSS. If you’re using `@wordpress/scripts`, you can configure it to process Tailwind. For this demonstration, we’ll focus on applying the classes.

Modify the build/index.css file to include any base styles or custom CSS that isn’t directly handled by Tailwind utilities. This file is enqueued for the editor.

/* build/index.css */
.wp-block-custom-lead-collector-lead-form {
    @apply border border-gray-300 rounded-lg shadow-sm;
}

.lead-collector-title {
    @apply text-2xl font-bold mb-4;
}

.lead-collector-form {
    @apply space-y-4;
}

.lead-collector-input-group {
    @apply flex flex-col;
}

.lead-collector-input-group label {
    @apply text-sm font-medium text-gray-700;
}

.lead-collector-input-group input[type="text"],
.lead-collector-input-group input[type="email"] {
    @apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm;
}

.lead-collector-submit-button {
    @apply w-full inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
}

The build/style-index.css file is enqueued for both the editor and the frontend. It should contain styles that are common to both environments.

/* build/style-index.css */
/* Global styles for the block */
.wp-block-custom-lead-collector-lead-form {
    /* Example: Ensure consistent box-sizing */
    box-sizing: border-box;
}

/* Specific styles for elements within the block */
.lead-collector-title {
    /* Example: Ensure title is responsive */
    word-break: break-word;
}

.lead-collector-form .lead-collector-input-group input {
    /* Example: Ensure inputs have a minimum height */
    min-height: 38px;
}

.lead-collector-submit-button {
    /* Example: Ensure button has a minimum height */
    min-height: 40px;
}

Now, let’s integrate these Tailwind classes into our JavaScript components. We’ll apply them directly to the JSX elements.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, ColorPicker, RangeControl } from '@wordpress/components';
import { useState } from '@wordpress/element';

// Import CSS
import './index.css';
import './style-index.css';

const Edit = ( { attributes, setAttributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    const blockProps = useBlockProps( {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
        className: 'wp-block-custom-lead-collector-lead-form', // Apply base block class
    } );

    const onChangeTitle = ( newTitle ) => {
        setAttributes( { titleText: newTitle } );
    };

    const onChangeButtonText = ( newButtonText ) => {
        setAttributes( { buttonText: newButtonText } );
    };

    const onChangeBackgroundColor = ( newColor ) => {
        setAttributes( { backgroundColor: newColor.hex } );
    };

    const onChangeTextColor = ( newColor ) => {
        setAttributes( { textColor: newColor.hex } );
    };

    const onChangePadding = ( value, side ) => {
        setAttributes( {
            padding: {
                ...padding,
                [ side ]: `${ value }px`,
            },
        } );
    };

    return (
        <>
            <InspectorControls>
                <PanelBody title="Lead Collector Settings" initialOpen={ true }>
                    <TextControl
                        label="Title Text"
                        value={ titleText }
                        onChange={ onChangeTitle }
                    />
                    <TextControl
                        label="Button Text"
                        value={ buttonText }
                        onChange={ onChangeButtonText }
                    />
                    <p>Background Color</p>
                    <ColorPicker
                        color={ backgroundColor }
                        onChangeComplete={ onChangeBackgroundColor }
                        disableAlpha
                    />
                    <p>Text Color</p>
                    <ColorPicker
                        color={ textColor }
                        onChangeComplete={ onChangeTextColor }
                        disableAlpha
                    />
                    <RangeControl
                        label="Padding Top (px)"
                        value={ parseInt( padding.top ) }
                        onChange={ ( value ) => onChangePadding( value, 'top' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Bottom (px)"
                        value={ parseInt( padding.bottom ) }
                        onChange={ ( value ) => onChangePadding( value, 'bottom' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                     <RangeControl
                        label="Padding Left (px)"
                        value={ parseInt( padding.left ) }
                        onChange={ ( value ) => onChangePadding( value, 'left' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                    <RangeControl
                        label="Padding Right (px)"
                        value={ parseInt( padding.right ) }
                        onChange={ ( value ) => onChangePadding( value, 'right' ) }
                        min={ 0 }
                        max={ 100 }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <RichText
                    tagName="h3"
                    value={ titleText }
                    onChange={ onChangeTitle }
                    placeholder="Enter title..."
                    className="lead-collector-title" // Tailwind class
                />
                <form className="lead-collector-form"> {/* Tailwind classes */}
                    <div className="lead-collector-input-group"> {/* Tailwind classes */}
                        <label htmlFor="lead-name">Name:</label>
                        <input type="text" id="lead-name" name="lead_name" placeholder="Your Name" />
                    </div>
                    <div className="lead-collector-input-group"> {/* Tailwind classes */}
                        <label htmlFor="lead-email">Email:</label>
                        <input type="email" id="lead-email" name="lead_email" placeholder="Your Email" />
                    </div>
                    <button type="submit" className="lead-collector-submit-button"> {/* Tailwind class */}
                        { buttonText }
                    </button>
                </form>
            </div>
        </>
    );
};

const Save = ( { attributes } ) => {
    const { titleText, buttonText, backgroundColor, textColor, padding } = attributes;

    const blockProps = {
        style: {
            backgroundColor: backgroundColor,
            color: textColor,
            paddingTop: padding.top,
            paddingBottom: padding.bottom,
            paddingLeft: padding.left,
            paddingRight: padding.right,
        },
        className: 'wp-block-custom-lead-collector-lead-form', // Standard WordPress block class
    };

    return (
        <div { ...blockProps }>
            <h3 className="lead-collector-title">{ titleText }</h3> {/* Tailwind class */}
            <form className="lead-collector-form"> {/* Tailwind classes */}
                <div className="lead-collector-input-group"> {/* Tailwind classes */}
                    <label htmlFor="lead-name-frontend">Name:</label>
                    <input type="text" id="lead-name-frontend" name="lead_name" placeholder="Your Name" />
                </div>
                <div className="lead-collector-input-group"> {/* Tailwind classes */}
                    <label htmlFor="lead-email-frontend">Email:</label>
                    <input type="email" id="lead-email-frontend" name="lead_email" placeholder="Your Email" />
                </div>
                <button type="submit" className="lead-collector-submit-button"> {/* Tailwind class */}
                    { buttonText }
                </button>
            </form>
        </div>
    );
};

registerBlockType( 'custom-lead-collector/lead-form', {
    edit: Edit,
    save: Save,
} );

The key here is that the Tailwind classes are applied directly to the elements. When the CSS is compiled, these classes will resolve to specific CSS properties. This approach ensures that the styling is encapsulated within the block’s generated CSS files (index.css and style-index.css), preventing conflicts with other site styles.

Dynamic Data Handling and Submission

For a lead collector, the primary dynamic aspect is data submission. While Gutenberg blocks primarily focus on presentation and editor interaction, actual data handling requires server-side logic. We’ll outline a common approach using AJAX.

First, we need to register a REST API endpoint in WordPress to receive the form data. Add this to your main plugin file (custom-lead-collector.php).

<?php
/**
 * ... (plugin header) ...
 */

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

// ... (block registration function) ...

/**
 * Registers a REST API endpoint for lead submission.
 */
function custom_lead_collector_register_api_route() {
    register_rest_route( 'custom-lead-collector/v1', '/submit', array(
        'methods'  => WP_REST_Server::CREATABLE, // Accepts POST requests
        'callback' => 'custom_lead_collector_handle_submission',
        'permission_callback' => '__return_true', // For simplicity, allow anyone. In production, implement proper permissions.
    ) );
}
add_action( 'rest_api_init', 'custom_lead_collector_register_api_route' );

/**
 * Handles the lead submission data.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_REST_Response Response object.
 */
function custom_lead_collector_handle_submission( WP_REST_Request $request ) {
    $name  = sanitize_text_field( $request->get_param( 'name' ) );
    $email = sanitize_email( $request->get_param( 'email' ) );

    if ( empty( $name ) || ! is_email( $email ) ) {
        return new WP_REST_Response( array( 'message' => 'Invalid data provided.' ), 400 );
    }

    // Here you would typically:
    // 1. Save the lead to a custom database table.
    // 2. Send an email notification to the site administrator.
    // 3. Send a confirmation email to the user (optional).
    // 4. Log the submission.

    // Example: Saving to post meta for a custom post type 'leads' (requires CPT registration)
    // $post_data = array(
    //     'post_title'    => sanitize_text_field( $name ) . ' - ' . $email,
    //     'post_status'   => 'publish',
    //     'post_type'     => 'lead', // Assuming 'lead' is a registered custom post type
    //     'meta_input'    => array(
    //         '_lead_name'  => $name,
    //         '_lead_email' => $email,
    //     ),
    // );
    // $post_id = wp_insert_post( $post_data );

    // For demonstration, we'll just return a success message.
    return new WP_REST_Response( array( 'message' => 'Lead submitted successfully!' ), 200 );
}
?>

Next, we need to add JavaScript to handle the form submission from the frontend. This JavaScript will intercept the form’s default submission, send the data to our REST API endpoint using `fetch`, and provide user feedback.

Add this JavaScript to your build/index.js file, typically within a separate function that runs on the frontend.

// ... (previous imports and Edit/Save components) ...

// Frontend script to handle form submission
function handleLeadFormSubmission() {
    const forms = document.querySelectorAll( '.wp-block-custom-lead-collector-lead-form .lead-collector-form' );

    forms.forEach( form => {
        form.addEventListener( 'submit', async ( event ) => {
            event.preventDefault(); // Prevent default form submission

            const formData = new FormData( form );
            const nameInput = form.querySelector( 'input[name="lead_name"]' );
            const emailInput = form.querySelector( 'input[name="lead_email"]' );

            const data = {
                name: nameInput.value,
                email: emailInput.value,
            };

            // Basic client-side validation
            if ( ! data.name || ! data.email || ! data.email.includes('@') ) {
                alert( 'Please enter a valid name and email address.' );
                return;
            }

            try {
                const response = await fetch( '/wp-json/custom-lead-collector/v1/submit', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': customLeadCollectorSettings.nonce, // If nonce is needed for authentication
                    },
                    body: JSON.stringify( data ),
                } );

                const result = await response.json();

                if ( response.ok ) {
                    alert( result.message || 'Thank you for your submission!' );
                    form.reset(); // Clear the form
                } else {
                    alert( result.message || 'An error occurred. Please try again later.' );
                }
            } catch ( error ) {
                console.error( 'Submission error:', error );
                alert( 'An unexpected error occurred. Please try again later.' );
            }
        } );
    } );
}

// Enqueue the frontend script
// This requires a wp_enqueue_script call in your PHP, typically in custom-lead-collector.php
// or a separate frontend assets file.
// For simplicity, we'll assume it's handled.
// To make this work, you'd need to pass the nonce to the frontend.
// Example PHP:
// wp_enqueue_script(
//     'custom-lead-collector-frontend',
//     plugins_url( 'build/frontend.js', __FILE__ ),
//     array( 'wp-element', 'wp-api-fetch' ), // wp-api-fetch is useful for nonce handling
//     filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
// );
// wp_localize_script( 'custom-lead-collector-frontend', 'customLeadCollectorSettings', array(
//     'nonce' => wp_create_nonce( 'wp_rest' ),
// ) );

// Call the submission handler when the DOM is ready
document.addEventListener( 'DOMContentLoaded', handleLeadFormSubmission );

// Re-register the block type with the same name
registerBlockType( 'custom-lead-collector/lead-form', {
    edit: Edit,
    save: Save,
} );

To make the frontend JavaScript work correctly, you need to enqueue it and localize necessary data (like nonces for security) using `wp_enqueue_script` and `wp_localize_script

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