Creating custom toggle toolbar buttons

A toggle button triggers an action when clicked and maintains an active state. This means it can be toggled on or off. The toggle button provides the user visual feedback of its state through CSS styling. An example of this behavior is the Bold, which becomes highlighted when the cursor is within text that has bold formatting.

Options

Name Value Requirement Description

text

string

optional

Text to display if no icon is found.

icon

string

optional

Name of the icon to be displayed. Must correspond to an icon: in the icon pack, in a custom icon pack, or added using the addIcon API.

tooltip

string

optional

Text for button tooltip.

enabled

boolean

optional

default: true - Represents the button’s enabled state. When false, the button is unclickable. Can be changed using setEnabled from the button’s API.

active

boolean

optional

default: false - Represents the button’s active state. When true, the button is highlighted. Can be changed using setActive from the button’s API.

onSetup

(api) => (api) => void

optional

default: () => () => {} - Function invoked when the button is rendered. For details, see: Using onSetup.

onAction

(api) => void

required

Function invoked when the button is clicked.

shortcut

string

optional

Shortcut to display in the tooltip. To register a shortcut, see: Add custom shortcuts to TinyMCE.

context

string

optional

default: mode:design - The context property dynamically enables or disables the button based on the editor’s current state. For details, see: Context.

API

Name Value Description

isEnabled

() => boolean

Checks if the button is enabled.

setEnabled

(state: boolean) => void

Sets the button’s enabled state.

isActive

() => boolean

Checks if the button is in the active (toggled on) state.

setActive

(state: boolean) => void

Sets the button’s active (toggled) state.

setText

(text: string) => void

Sets the text label to display.

setIcon

(icon: string) => void

Sets the icon of the button.

Toggle button example and explanation

  • TinyMCE

  • HTML

  • JS

  • Edit on CodePen

<textarea id="custom-toolbar-toggle-button">
  <p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
  <h2 style="text-align: center;">Welcome to the TinyMCE editor demo!</h2>
  <p>Select a menu item from the listbox above and it will insert contents into the editor at the caret position.</p>

  <h2>Got questions or need help?</h2>
  <ul>
    <li>Our <a href="https://www.tiny.cloud/docs/tinymce/8/">documentation</a> is a great resource for learning how to configure TinyMCE.</li>
    <li>Have a specific question? Try the <a href="https://stackoverflow.com/questions/tagged/tinymce" target="_blank" rel="noopener"><code>tinymce</code> tag at Stack Overflow</a>.</li>
    <li>We also offer enterprise grade support as part of <a href="https://www.tiny.cloud/pricing">TinyMCE premium plans</a>.</li>
  </ul>

  <h2>Found a bug?</h2>
  <p>If you think you have found a bug please create an issue on the <a href="https://github.com/tinymce/tinymce/issues">GitHub repo</a> to report it to the developers.</p>

  <h2>Finally ...</h2>
  <p>Need file uploads in your app? Consider using <a href="https://www.tiny.cloud/docs/tinymce/latest/uploadcare/" target="_blank" rel="noopener noreferrer">Uploadcare</a> with TinyMCE for a fast, modern upload experience.</p>
  <p>Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    <br>All the best from the TinyMCE team.</p>
</textarea>
tinymce.init({
  selector: 'textarea#custom-toolbar-toggle-button',
  toolbar: 'customStrikethrough customToggleStrikethrough',
  setup: (editor) => {
    editor.ui.registry.addToggleButton('customStrikethrough', {
      text: 'Strikethrough',
      onAction: (api) => {
        editor.execCommand('mceToggleFormat', false, 'strikethrough');
        api.setActive(!api.isActive());
      }
    });

    editor.ui.registry.addToggleButton('customToggleStrikethrough', {
      icon: 'strike-through',
      onAction: (_) => editor.execCommand('mceToggleFormat', false, 'strikethrough'),
      onSetup: (api) => {
        api.setActive(editor.formatter.match('strikethrough'));
        const changed = editor.formatter.formatChanged('strikethrough', (state) => api.setActive(state));
        return () => changed.unbind();
      }
    });
  },
  content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }'
});

The example above adds two custom strikethrough toggle buttons. Both buttons use the mceToggleFormat command to apply and remove strikethrough formatting. This command toggles a specified format on and off, but only works for formats already registered with the editor. In this example, strikethrough is the registered format.

Basic togle: manual state management

The first (customStrikethrough) button applies and removes strikethrough formatting. Its state toggles upon click using api.setActive(!api.isActive()). However, this button does not reflect whether the selected content has strikethrough formatting which is its expected behavior. Moving the cursor into content with strikethrough formatting does not activate the button, and moving it out does not deactivate it.

State-synced toggle: automatic state updates

The second button (customToggleStrikethrough) addresses this by using editor.formatter.formatChanged in its onSetup callback to monitor the formatting state of the current selection.

Note: The format name passed to mceToggleFormat via editor.execCommand(command, ui, args) is the same as the one used in editor.formatter.formatChanged(formatName, callback).

The formatChanged method accepts the following parameters:

The formatChanged method accepts the following parameters:

  • formats (String, required) — The name of the registered format to monitor.

  • callback (Function, required) — A function called when the formatting state changes. The callback receives a state boolean indicating whether the format is present (true) or absent (false) in the current selection.

  • similar (Boolean, optional) — When true, treats all similar variants of the same format name as equivalent, regardless of variables. Defaults to false.

  • vars (Object, optional) — When similar is false, specifies which format variables must match for the callback to execute.

The method returns an object with an unbind function. Calling unbind() removes the format listener, which is essential for cleanup when the button is destroyed.

In the example, onSetup first checks if the current selection matches strikethrough formatting using editor.formatter.match('strikethrough') and sets the initial active state accordingly. It then registers a formatChanged listener that calls api.setActive(state) whenever the strikethrough state changes. The teardown function returned from onSetup calls changed.unbind() to clean up the listener.

This approach ensures customToggleStrikethrough is highlighted whenever the cursor is within strikethrough-formatted content and deactivated when it is not, regardless of how the formatting was applied.

Using onSetup

onSetup accepts a function that receives the component’s API. This function should return a callback that returns nothing after being passed the component’s API. This occurs because onSetup runs whenever the component is rendered, and the callback returned by onSetup is executed when the component is destroyed. The function returned from onSetup is essentially an onTeardown handler, and can be used to unbind events and callbacks.

To clarify, in code onSetup may look like this:

onSetup: (api) => {
  // Runs when the component is created
  // Configure the component or bind event listeners

  return (api) => {
    // Runs when the component is destroyed
    // Unbind event listeners or clean up resources
  };
};

To bind a callback function to an editor event use +editor.off(eventName, callback)`]. To unbind an event listener use `xref:apis/tinymce.editor.adoc#off[`+editor.off(eventName, callback). Any event listeners should be unbound in the teardown callback. The only editor event which does not need to be unbound is `init e.g. editor.on('init', callback).

  • The callback function passed to editor.off() should be the same function passed to editor.on(). For example, if an editorEventCallback function is bound to the NodeChange event when the button is created, onSetup should return (api) => editor.off('NodeChange', editorEventCallback).

  • If onSetup does not register any event listeners or only listens to the init event, onSetup can return an empty function e.g. return () => {};.