How to add a button to the classic editor toolbar with JS in WordPress

Contents

Introduction

This article gives a complete, detailed, reproducible tutorial for adding a custom button to the WordPress Classic Editor toolbar using JavaScript (TinyMCE) plus the minimal required PHP hooks. It covers prerequisites, how WordPress expects TinyMCE plugins to be registered, permission checks, i18n, adding an icon, creating an editor modal or inserting content directly, CSS for button styling, debugging tips, and compatibility considerations. All example source code is provided and ready to drop into a theme or plugin.

Prerequisites and context

  • Classic Editor or Classic block active: This applies to the TinyMCE-based Classic Editor (the editor used before the block editor, or when the Classic Editor plugin is active).
  • Familiarity: You need basic PHP for adding hooks (functions.php or a plugin file) and basic JavaScript to write the TinyMCE plugin.
  • TinyMCE version: WordPress ships TinyMCE 4.x/5.x depending on WP version the approach using tinymce.PluginManager.add works across them.
  • Where to place files: You can add code in your themes functions.php for tests, but production best practice is to create a small plugin to keep editor customizations independent of a theme.

High-level steps

  1. Register the button via the WordPress filters mce_buttons (to add the button name to the toolbar) and mce_external_plugins (to tell TinyMCE where to load the plugin JavaScript).
  2. Create the TinyMCE plugin JavaScript file that registers a button with tinymce.PluginManager.add and implements the onClick behavior (insert content, open a small modal, or open a custom window).
  3. Optionally add CSS for an icon using the standard mce-i-{buttonname} class or provide an icon via a SVG or background-image URL.
  4. Enqueue or otherwise make the plugin JS available only to users that can use the visual editor (check get_user_option(rich_editing) and current_user_can).
  5. Consider localization (i18n), security, and compatibility.

Detailed PHP: register the TinyMCE button and plugin file

The PHP code below goes in a plugin file or your themes functions.php. It:

  • Checks that the current user can edit posts/pages and has the rich editor enabled
  • Adds a custom button name to the first row of TinyMCE buttons
  • Tells TinyMCE the URL of the external plugin JS file
  • Optionally localizes data (strings, nonces) to the JS file.
 URL to the JS file
    plugins[my_custom_button] = plugins_url( /js/my-tinymce-plugin.js, __FILE__ )
    return plugins
}

/
  Enqueue and register the filters conditionally.
 /
function myplugin_mce_integration() {
    // Only load for users who can edit and have the visual rich editor enabled
    if ( ! current_user_can( edit_posts )  ! current_user_can( edit_pages ) ) {
        return
    }
    if ( get_user_option( rich_editing ) !== true ) {
        return
    }

    add_filter( mce_buttons, myplugin_register_tinymce_button )
    add_filter( mce_external_plugins, myplugin_add_tinymce_plugin )

    // Example: pass localized strings or a nonce to the JS plugin
    wp_localize_script(
        myplugin-mce-plugin-placeholder, // not enqueued, just a handle for localization used by your plugin JS if you enqueue it separately
        MyPluginMCE,
        array(
            insertText => __( Inserted by My Button, my-plugin-textdomain ),
            nonce      => wp_create_nonce( myplugin_mce_nonce ),
        )
    )
}
add_action( admin_head, myplugin_mce_integration )
?>

Notes about the PHP sample:

  • plugins_url( … , __FILE__ ) assumes you are in a plugin file. If you add the JS file inside your theme directory, adjust the URL (use get_template_directory_uri() or get_stylesheet_directory_uri()).
  • Localization: In this snippet we show wp_localize_script as an example. For the TinyMCE external plugin loaded via mce_external_plugins, you can instead pass data by outputting inline JS in admin_head or by registering/enqueuing a script and localizing it. The important point is to make translatable strings available to the plugin.
  • Hook timing: Using admin_head is fine for adding the filters conditionally. Some prefer admin_init or init with proper capability checks. The key is filters must be added before TinyMCE is initialized for the current admin page.

TinyMCE plugin JavaScript: register the button and behavior

Create a JS file (the path referenced in the PHP). The file registers a TinyMCE plugin via tinymce.PluginManager.add and defines a button that inserts content or opens a TinyMCE window. Replace the example insert content to match the content or shortcode you need.

(function() {
    // The name my_custom_button must match the button slug added via mce_buttons
    tinymce.PluginManager.add(my_custom_button, function(editor, url) {

        // Add a button that inserts a simple snippet
        editor.addButton(my_custom_button, {
            title: Insert special content, // tooltip, consider i18n can be replaced with localized data
            icon: my-custom-icon, // a CSS class that you can style (see CSS section)
            cmd: my_custom_button_command,
            onclick: function() {
                // Simple insertion example:
                editor.insertContent(
Hello World
) } }) // Optional: Add a command that can be triggered from the button or other UI editor.addCommand(my_custom_button_command, function() { // Example: open a TinyMCE dialog window with form fields editor.windowManager.open({ title: Insert formatted block, body: [ { type: textbox, name: title, label: Title }, { type: textbox, name: subtitle, label: Subtitle } ], onsubmit: function( e ) { // e.data contains the submitted fields var content =

editor.dom.encode(e.data.title)

content =

editor.dom.encode(e.data.subtitle)

editor.insertContent(content) } }) }) // Return metadata (optional) return { getMetadata: function () { return { name: My Custom Button, url: https://example.com } } } }) })()

Explanation of the JS:

  • tinymce.PluginManager.add registers the plugin using the same key used in the PHP mce_external_plugins mapping.
  • editor.addButton defines the button UI. You can supply an icon class, title, and onclick handler. You may also call editor.addCommand and assign a cmd property so that the button triggers that command.
  • editor.windowManager.open creates a small modal dialog managed by TinyMCE. The body parameter uses TinyMCE 4 dialog configuration (textbox, listbox, etc.). For TinyMCE 5 behavior may differ slightly, but the same general approach works in WordPress classic editor.
  • Use editor.dom.encode when inserting user-provided content to avoid accidental HTML injection.

Button icon and CSS

TinyMCE buttons in the classic editor use a CSS class naming convention mce-i-{iconname}. In the example above we used icon: my-custom-icon so the CSS class will be mce-i-my-custom-icon. You can style it with a background image or an SVG data URI.

/ Example CSS - place in admin stylesheet or enqueue for admin /
.mce-i-my-custom-icon {
    background-image: url(data:image/svg xmlutf8,ltsvg xmlns=http://www.w3.org/2000/svg width=20 height=20 viewBox=0 0 20 20gtltrect width=20 height=20 fill=#007cba/gtlttext x=10 y=14 font-size=10 text-anchor=middle fill=#fffgtBlt/textgtlt/svggt)
    background-repeat: no-repeat
    background-size: 16px 16px
}
/ You can also use a PNG or SVG file URL instead of a data URI /

Important notes:

  • If you add CSS, ensure it only loads in the editor/admin use admin_enqueue_scripts to enqueue the style for the post editing screen only.
  • The class used by TinyMCE is precisely mce-i-{icon}. If your icon name includes dashes or underscores ensure CSS matches exactly.

Advanced example: insert a shortcode and support server-side rendering

If your button inserts a shortcode that your theme or plugin will process server-side, keep the JS simple and insert the shortcode text. Example:

// Minimal handler to insert a shortcode
editor.addButton(my_custom_button, {
    title: Insert Alert Shortcode,
    icon: my-custom-icon,
    onclick: function() {
        var shortcode = [my_alert type=warning]This is an alert[/my_alert]
        editor.insertContent(shortcode)
    }
})

Then in PHP you create a corresponding shortcode handler (do_shortcode) to render the front-end HTML. That keeps the editor lightweight and leverages existing rendering logic.

// Example shortcode handler in your plugin
function my_alert_shortcode( atts, content =  ) {
    atts = shortcode_atts( array(
        type => info,
    ), atts, my_alert )

    class = my-alert  . esc_attr( atts[type] )
    return 
. wp_kses_post( content ) .
} add_shortcode( my_alert, my_alert_shortcode )

Security and capability checks

  • Only show the button to users who can edit content. Use current_user_can(edit_posts) and/or edit_pages as relevant.
  • When inserting content based on user input, sanitize or encode values. Use editor.dom.encode in JS for simple encoding and server-side sanitization for shortcodes or server processing (esc_html, esc_attr, wp_kses, etc.).
  • If your JS needs to call admin-ajax.php or REST API, pass a nonce via wp_localize_script and verify it server-side with check_ajax_referer or wp_verify_nonce.

Enqueueing CSS or extra scripts in admin

To ensure your CSS only runs in the admin edit screen, enqueue it conditionally:

function myplugin_enqueue_admin_assets( hook ) {
    // Load only on post.php and post-new.php (edit screens)
    if ( post.php !== hook  post-new.php !== hook ) {
        return
    }

    // Example CSS for your icon
    wp_enqueue_style( myplugin-editor-style, plugins_url( /css/my-editor-style.css, __FILE__ ) )
}
add_action( admin_enqueue_scripts, myplugin_enqueue_admin_assets )

Debugging tips

  1. Inspect the browser console for JS errors. If tinymce is undefined, ensure your mce_ filters are added early enough and that youre on the visual editor page.
  2. Confirm the plugin JS URL is correct by navigating to the URL you output in mce_external_plugins in a browser tab—should return the JS file.
  3. Make sure get_user_option(rich_editing) returns true for your user. If you have the Visual Editor unchecked in the user profile, the button will not appear.
  4. Check that your button name in mce_buttons matches the name used in addButton and the key in mce_external_plugins.
  5. Use the element inspector to find the toolbar and check for the button HTML and CSS class names.

Compatibility and best practices

  • Prefer a plugin over theme functions: If this button is functionality-related rather than presentational, implement it in a plugin so it persists when switching themes.
  • Localization: Use __() and _x() in PHP and pass strings to the JS plugin via wp_localize_script for translations.
  • Defer complex UIs: For complex settings dialogs, consider opening a modal that loads a real HTML form from the server (via a windowManager.open with an external URL or by using ThickBox/WordPress modal techniques). For simple insertions use editor.windowManager.open.
  • Gutenberg: If your site uses the block editor, consider also providing an equivalent block or a block-enabled toolbar plugin. The Classic Editor plugin will keep TinyMCE, otherwise blocks are preferred.

Troubleshooting common problems

  • Button doesnt show: Confirm youre on the Visual tab and the current user has rich editing enabled. Confirm your mce_filters are run by using error_log or temporarily echoing a script ensure they run in admin context.
  • Plugin JS 404: Verify plugins_url or get_template_directory_uri path is correct and file exists.
  • Dialog fields not returning values: Different TinyMCE versions changed the dialog API. Use console.log to inspect e.data in onsubmit, or implement a simple insertion without a dialog to isolate the issue.
  • Icon not visible: Confirm the .mce-i-{icon} class style is loaded and not overwritten. Use the inspector to view computed background-image.

Complete minimal plugin structure (example)

Suggested files for packaging as a small plugin:

  • my-classic-button/
  • my-classic-button/my-classic-button.php (the PHP bootstrap with the mce filters)
  • my-classic-button/js/my-tinymce-plugin.js (the TinyMCE plugin JS)
  • my-classic-button/css/my-editor-style.css (optional CSS for icon)

Putting it all together as files and hooks makes the button portable and safe to use across environments.

Final checklist before deploying

  • Scope assets to admin/edit screens so they are not loaded on the front-end unnecessarily.
  • Test with different user roles (editor, admin, contributor) to confirm permission logic.
  • Test with rich editing turned off to ensure your code doesnt load incorrectly.
  • Internationalize strings used in UI and localize them to JS when needed.
  • Document the inserted content format and how it should be handled on the front-end (shortcode rendering or HTML structure).

Useful reference links

For implementation details consult the official WordPress developer docs and TinyMCE docs. Example pages: https://developer.wordpress.org/reference/hooks/mce_external_plugins/, https://www.tiny.cloud/docs/.

End of article



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *