• 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 automatic translation switcher block for Gutenberg using Tailwind CSS isolated elements

Step-by-Step Guide to building a custom automatic translation switcher block for Gutenberg using Tailwind CSS isolated elements

Gutenberg Block Architecture for a Custom Translation Switcher

Developing a custom Gutenberg block for a translation switcher requires a robust architecture that balances user experience with maintainability. This guide focuses on building a block that leverages Tailwind CSS for isolated styling, ensuring it doesn’t conflict with global theme styles. We’ll cover the essential components: the PHP registration, JavaScript for the editor interface, and the frontend rendering logic.

Plugin Setup and Block Registration (PHP)

Begin by creating a simple WordPress plugin. This plugin will house our custom Gutenberg block. The core of the block registration happens in PHP, defining its attributes, editor script, and frontend render callback.

Create a directory for your plugin, e.g., custom-translation-switcher, within wp-content/plugins/. Inside, create a main plugin file, e.g., custom-translation-switcher.php.

Plugin File: custom-translation-switcher.php

<?php
/**
 * Plugin Name: Custom Translation Switcher Block
 * Description: A custom Gutenberg block for a translation switcher with isolated Tailwind CSS.
 * Version: 1.0.0
 * Author: Antigravity
 * License: GPL-2.0-or-later
 * Text Domain: custom-translation-switcher
 */

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

Block Metadata and Asset Enqueueing (block.json)

WordPress 5.0+ uses block.json for block registration and asset management. This file defines the block’s name, title, icon, attributes, and crucially, the JavaScript and CSS files to be enqueued for the editor and frontend.

Block Metadata File: block.json

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "custom-translation-switcher/block",
    "version": "1.0.0",
    "title": "Translation Switcher",
    "category": "widgets",
    "icon": "translation",
    "description": "A custom block to switch between different language versions of content.",
    "keywords": [ "translation", "language", "switcher", "multilingual" ],
    "attributes": {
        "languages": {
            "type": "array",
            "default": [
                { "code": "en", "name": "English", "url": "/" },
                { "code": "fr", "name": "Français", "url": "/fr/" }
            ]
        },
        "defaultLanguage": {
            "type": "string",
            "default": "en"
        },
        "showFlags": {
            "type": "boolean",
            "default": true
        },
        "showNames": {
            "type": "boolean",
            "default": true
        }
    },
    "textdomain": "custom-translation-switcher",
    "editorScript": "file:./build/index.js",
    "editorStyle": "file:./build/index.css",
    "style": "file:./build/style-index.css",
    "viewScript": "file:./build/view.js"
}

Frontend Rendering (PHP Callback)

The render_callback in block.json (or explicitly defined in PHP) handles how the block is rendered on the frontend. For our translation switcher, this callback will iterate through the configured languages and output the necessary HTML, including links to different language versions. We’ll use a PHP function to generate this output.

PHP Render Callback Function (within custom-translation-switcher.php)

 'render_custom_translation_switcher_block',
//     ) );
// } );
// However, the block.json approach is cleaner. The `register_block_type( __DIR__ . '/build' );`
// will automatically pick up the `render` key if present in block.json, or use the default
// rendering if no `render` key is specified and a `render_callback` is defined in PHP.
// For clarity, we'll assume block.json handles it. If not, uncomment the above `add_action`.
?>

Note: The render_custom_translation_switcher_block function assumes that flag images (e.g., en.svg, fr.svg) are placed in a directory named assets/flags/ within your plugin. You’ll need to create this directory and add your SVG flag files.

Editor Interface (JavaScript)

The editor experience is crucial for content creators. We’ll use React and the WordPress Block Editor API to build the block’s interface in the editor. This involves defining the block’s attributes, rendering the controls for setting languages, flags, and names, and providing a preview of the block.

Build Process Setup

To compile modern JavaScript (ESNext) and JSX into browser-compatible code, we need a build process. WordPress’s official `@wordpress/scripts` package is the standard for this. Install it as a development dependency:

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

Add build scripts to your package.json:

{
  "name": "custom-translation-switcher",
  "version": "1.0.0",
  "description": "",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "packages-update": "wp-scripts packages-update"
  },
  "keywords": [],
  "author": "",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.10.0"
  }
}

Run npm run build to compile your JavaScript and CSS. This will create the build/ directory specified in block.json.

Editor JavaScript: src/index.js

This file is the entry point for your block’s editor script. It imports necessary WordPress components and defines the block’s behavior.

/**
 * WordPress dependencies.
 */
import { registerBlockType } from '@wordpress/blocks';
import {
    useBlockProps,
    InspectorControls,
    MediaUpload,
    MediaUploadCheck,
} from '@wordpress/block-editor';
import { PanelBody, Button, TextControl, ToggleControl, SelectControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { upload } from '@wordpress/icons'; // Example icon

/**
 * Internal dependencies.
 */
import metadata from '../block.json';
import './style.scss'; // For editor styles
import './editor.scss'; // For editor specific styles

const Edit = ( { attributes, setAttributes } ) => {
    const blockProps = useBlockProps();
    const { languages, defaultLanguage, showFlags, showNames } = attributes;

    // Ensure languages is always an array
    useEffect(() => {
        if (!Array.isArray(languages)) {
            setAttributes({ languages: metadata.attributes.languages.default });
        }
    }, [languages, setAttributes]);

    const addLanguage = () => {
        const newLanguages = [...languages, { code: '', name: '', url: '' }];
        setAttributes({ languages: newLanguages });
    };

    const updateLanguage = ( index, key, value ) => {
        const newLanguages = [...languages];
        newLanguages[index][key] = value;
        setAttributes({ languages: newLanguages });
    };

    const removeLanguage = ( index ) => {
        const newLanguages = languages.filter((_, i) => i !== index);
        setAttributes({ languages: newLanguages });
    };

    const handleFlagUpload = (index, media) => {
        if (media && media.url) {
            const newLanguages = [...languages];
            // We are not storing the media ID, just the URL for simplicity in this example.
            // For robust solutions, consider storing media ID and generating URL server-side or via API.
            newLanguages[index].flagUrl = media.url;
            setAttributes({ languages: newLanguages });
        }
    };

    // Get available language codes for default language selection
    const languageOptions = languages.map(lang => ({
        label: lang.name || lang.code || __('Untitled Language', 'custom-translation-switcher'),
        value: lang.code || ''
    })).filter(option => option.value !== '');

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Language Settings', 'custom-translation-switcher' ) } initialOpen={ true }>
                    <p>{ __( 'Configure the languages for the switcher.', 'custom-translation-switcher' ) }</p>
                    { languages.map( ( lang, index ) => (
                        <div key={ index } style={ { marginBottom: '15px', border: '1px solid #eee', padding: '10px' } }>
                            <TextControl
                                label={ __( 'Language Code (e.g., en, fr)', 'custom-translation-switcher' ) }
                                value={ lang.code }
                                onChange={ ( value ) => updateLanguage( index, 'code', value ) }
                            />
                            <TextControl
                                label={ __( 'Language Name (e.g., English)', 'custom-translation-switcher' ) }
                                value={ lang.name }
                                onChange={ ( value ) => updateLanguage( index, 'name', value ) }
                            />
                            <TextControl
                                label={ __( 'URL for this language', 'custom-translation-switcher' ) }
                                value={ lang.url }
                                onChange={ ( value ) => updateLanguage( index, 'url', value ) }
                                help={ __( 'e.g., /fr/ or https://fr.example.com', 'custom-translation-switcher' ) }
                            />
                            <MediaUploadCheck>
                                <MediaUpload
                                    onSelect={ ( media ) => handleFlagUpload( index, media ) }
                                    allowedTypes={ [ 'image' ] }
                                    value={ lang.mediaId } // If you were storing media ID
                                    render={ ( { open } ) => (
                                        <Button
                                            icon={ upload }
                                            onClick={ open }
                                            variant="secondary"
                                            isSmall
                                        >
                                            { __( 'Upload Flag', 'custom-translation-switcher' ) }
                                        </Button>
                                    ) }
                                />
                            </MediaUploadCheck>
                            { lang.flagUrl && (
                                <img src={ lang.flagUrl } alt={ lang.name } style={ { maxWidth: '30px', marginTop: '5px' } } />
                            ) }
                            <Button
                                isDestructive
                                onClick={ () => removeLanguage( index ) }
                                style={ { marginTop: '10px' } }
                            >
                                { __( 'Remove Language', 'custom-translation-switcher' ) }
                            </Button>
                        </div>
                    ) ) }
                    <Button variant="primary" onClick={ addLanguage } isSmall>
                        { __( 'Add Language', 'custom-translation-switcher' ) }
                    </Button>
                </PanelBody>
                <PanelBody title={ __( 'Display Settings', 'custom-translation-switcher' ) } initialOpen={ false }>
                    <ToggleControl
                        label={ __( 'Show Flags', 'custom-translation-switcher' ) }
                        checked={ showFlags }
                        onChange={ ( value ) => setAttributes( { showFlags: value } ) }
                    />
                    <ToggleControl
                        label={ __( 'Show Language Names', 'custom-translation-switcher' ) }
                        checked={ showNames }
                        onChange={ ( value ) => setAttributes( { showNames: value } ) }
                    />
                    <SelectControl
                        label={ __( 'Default Language', 'custom-translation-switcher' ) }
                        value={ defaultLanguage }
                        options={ languageOptions }
                        onChange={ ( value ) => setAttributes( { defaultLanguage: value } ) }
                        help={ __( 'Select the default language for this page.', 'custom-translation-switcher' ) }
                    />
                </PanelBody>
            </InspectorControls>

            <div { ...blockProps } className={ `${blockProps.className} editor-preview` }>
                <h4>{ __( 'Translation Switcher Preview', 'custom-translation-switcher' ) }</h4>
                <ul className="translation-languages">
                    { languages.map( ( lang, index ) => {
                        const isDefault = ( lang.code === defaultLanguage );
                        const activeClass = isDefault ? ' is-default-language' : '';
                        const flagUrl = lang.flagUrl || ( showFlags && `/wp-content/plugins/custom-translation-switcher/assets/flags/${lang.code}.svg` ); // Fallback to plugin path
                        const flagHtml = flagUrl ? `<img src="${flagUrl}" alt="${lang.name || lang.code} flag" class="language-flag" />` : '';
                        const nameHtml = showNames ? `<span class="language-name">${lang.name || lang.code}</span>` : '';

                        return (
                            <li key={ index } className={ `language-item${activeClass}` }>
                                <a href={ lang.url || '#' }>
                                    { flagHtml }
                                    { nameHtml }
                                </a>
                            </li>
                        );
                    } ) }
                </ul>
            </div>
        </>
    );
};

registerBlockType( metadata.name, {
    edit: Edit,
    save: () => {
        // The save function should return null because the rendering is handled by PHP's render_callback.
        // This is known as a "server-side rendered" or "dynamic" block.
        return null;
    },
} );

Editor Styles: src/editor.scss

Styles specific to the editor. These won’t load on the frontend.

.wp-block-custom-translation-switcher.editor-preview {
    border: 1px dashed #ccc;
    padding: 10px;
    background-color: #f9f9f9;
    .translation-languages {
        display: flex;
        list-style: none;
        padding: 0;
        margin: 0;
        .language-item {
            margin-right: 10px;
            a {
                text-decoration: none;
                color: #333;
                display: flex;
                align-items: center;
                .language-flag {
                    width: 20px;
                    height: auto;
                    margin-right: 5px;
                }
                .language-name {
                    font-size: 0.9em;
                }
            }
            &.is-default-language {
                font-weight: bold;
            }
        }
    }
}

Frontend Styles: src/style.scss

These styles apply to both the editor and the frontend. They are crucial for the block’s appearance.

.wp-block-custom-translation-switcher {
    .translation-languages {
        display: flex;
        list-style: none;
        padding: 0;
        margin: 0;
        .language-item {
            margin-right: 15px;
            a {
                text-decoration: none;
                color: #333;
                display: flex;
                align-items: center;
                transition: color 0.2s ease-in-out;

                &:hover {
                    color: #0073aa; // WordPress blue for links
                }

                .language-flag {
                    width: 24px; // Slightly larger flags on frontend
                    height: auto;
                    margin-right: 8px;
                    border: 1px solid #eee; // Subtle border for flags
                    box-shadow: 0 1px 2px rgba(0,0,0,0.1);
                }
                .language-name {
                    font-size: 1em;
                    font-weight: 500;
                }
            }
            &.is-default-language {
                font-weight: bold;
                a {
                    color: #005177; // Darker blue for default
                }
            }
        }
    }
}

Isolated Tailwind CSS Integration

To ensure Tailwind CSS styles are isolated to our block and don’t affect the rest of the site, we’ll use Tailwind’s JIT (Just-In-Time) mode and configure it to scan only our block’s files. This requires a tailwind.config.js file and a postcss.config.js file.

Tailwind Configuration: tailwind.config.js

Place this file in the root of your plugin directory (alongside package.json).

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './**/*.php', // Scan all PHP files for classes
    './src/**/*.js', // Scan JS files for classes
    './src/**/*.jsx', // Scan JSX files for classes
    // Add any other relevant file types if necessary
  ],
  theme: {
    extend: {},
  },
  plugins: [],
  // Prefix all generated classes to prevent global scope pollution
  prefix: 'tw-',
  // Ensure Tailwind CSS is applied only within a specific scope if needed
  // For example, if you want to scope it to a parent element like '.wp-block-custom-translation-switcher'
  // This is more advanced and might require custom PostCSS setup.
  // For simpler isolation, the prefix and careful scanning are key.
};

PostCSS Configuration: postcss.config.js

This file tells PostCSS how to process your CSS, including Tailwind.

module.exports = {
  plugins: {
    'tailwindcss/nesting': {},
    tailwindcss: {},
    autoprefixer: {},
  },
};

Integrating Tailwind into the Build Process

We need to modify the build process to include Tailwind CSS. The easiest way is to add a custom PostCSS step. Update your package.json scripts:

{
  "name": "custom-translation-switcher",
  "version": "1.0.0",
  "description": "",
  "main": "build/index.js",
  "scripts": {
    "build": "npm run build:css && wp-scripts build",
    "start": "npm run start:css & wp-scripts start",
    "build:css": "postcss src/style.scss --dir build --output style-index.css --config postcss.config.js",
    "start:css": "postcss src/style.scss --dir build --output style-index.css --config postcss.config.js --watch",
    "packages-update": "wp-scripts packages-update"
  },
  "keywords": [],
  "author": "",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.10.0",
    "autoprefixer": "^10.4.16",
    "postcss": "^8.4.32",
    "postcss-cli": "^10.1.0",
    "postcss-loader": "^7.3.4",
    "tailwindcss": "^3.3.6",
    "tailwindcss/nesting": "^0.0.0-compat.1"
  }
}

Install the new development dependencies:

npm install

Now, when you run npm run build, PostCSS will process src/style.scss, including Tailwind directives, and output build/style-index.css. The prefix: 'tw-' in tailwind.config.js will prepend tw- to all generated Tailwind classes.

Applying Tailwind Classes in Block Code

In your src/index.js and src/style.scss, use Tailwind classes prefixed with tw-. For example, instead of flex, use tw-flex. Instead of items-center, use tw-items-center.

Example: Modifying src/style.scss for Tailwind

/* Import Tailwind directives */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Your block's styles */
.wp-block-custom-translation-switcher {
    @apply tw-text-gray-800; /* Example Tailwind class */

    .translation-languages {
        @apply tw-flex tw-list-none tw-p-0 tw-m-0;

        .language-item {
            @apply tw-mr-4; /* Tailwind margin */

            a {
                @apply tw-no-underline tw-text-gray-700 tw-flex tw-items-center tw-transition-colors tw-duration-200 tw-ease-in-out;

                &:hover {
                    @apply tw-text-blue-600; /* Tailwind hover color */
                }

                .language-flag {
                    @apply tw-w-6 tw-h-auto tw-mr-2 tw-border tw-border-gray-200 tw-shadow-sm tw-rounded-sm; /* Tailwind styling */
                }
                .language-name {
                    @apply tw-text-sm tw-font-medium; /* Tailwind text size and weight */
                }
            }

            &.is-default-language {
                @apply tw-font-bold;
                a {
                    @apply tw-text-blue-800;
                }
            }
        }
    }
}

Similarly, in src/editor.scss, use prefixed Tailwind classes. The editor-preview class in the editor styles can also be styled with Tailwind.

Frontend JavaScript (view.js)

While the primary rendering is server-side, you might need JavaScript for frontend interactivity (e.g., dynamic dropdowns, AJAX loading). The viewScript in block.json handles this. For a simple switcher, this might be minimal or even unnecessary if all links are static.

Frontend JavaScript: src/view.js

/**
 * Frontend JavaScript for the Custom Translation Switcher block.
 * This script runs on the frontend of the website.
 */

document.addEventListener( 'DOMContentLoaded', () => {
    // Example: Add a class to the body when the switcher is present
    const switcherBlocks = document.querySelectorAll( '.wp-block-custom-translation-switcher' );

    if ( switcherBlocks.length > 0 ) {
        document.body.classList.add( 'has-translation-switcher' );
    }

    // Add any other frontend interactivity here.
    // For instance, if you wanted a dropdown instead of a list:
    // switcherBlocks.forEach( block => {
    //     const list = block.querySelector( '.translation-languages' );
    //     if ( list ) {
    //         // Logic to convert list to dropdown
    //     }
    // } );
} );

Ensure viewScript is correctly set in block.json to point to the compiled build/view.js.

Deployment and Testing

After setting up all files:

  • Run npm run build in your plugin’s directory. This compiles JS, CSS, and processes Tailwind.
  • Activate the “Custom Translation Switcher Block” plugin in your WordPress admin.
  • Go to the WordPress editor, add the “Translation Switcher” block, and configure its languages, flags, and display options.
  • Save the post/page and view it on the frontend to verify the rendering and styling.
  • Check the HTML source to ensure no unintended global CSS classes are applied.

Advanced Considerations for Enterprise

For enterprise-level applications, consider these enhancements:

  • Internationalization (i18n): Ensure all user-facing strings are translatable using WordPress’s i18n functions (__, _x, etc.) and a text domain.
  • Performance: Optimize flag images. Consider lazy loading or using CSS sprites. Ensure the view.js is small and efficient.
  • Accessibility: Use ARIA attributes where appropriate, ensure sufficient color contrast, and keyboard navigability.
  • Dynamic Language Loading: For complex sites, instead of hardcoding URLs, fetch language data via the REST API or a custom endpoint.
  • Caching: Ensure the server-side rendered output is cache-friendly.
  • Security: Sanitize all user inputs and escape all outputs properly, especially URLs and text content.
  • Integration with Translation Plugins: If using a plugin like WPML or Polylang, adapt the block to integrate with their APIs for language detection and URL generation.
  • Configuration Management: For large-scale deployments, consider managing block configurations centrally rather than per-post.

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