---
title: "Creating custom toggle toolbar buttons"
description: "Creating custom toggle toolbar buttons for TinyMCE"
canonical_url: "https://www.tiny.cloud/docs/tinymce/latest/custom-toggle-toolbar-button/"
md_url: "https://www.tiny.cloud/docs/tinymce/latest/custom-toggle-toolbar-button/index.md"
version: "latest"
last_updated: "2026-09-24T22:40:35Z"
tokens: 1824
---
# 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** button, 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](../editor-icon-identifiers/), in a [custom icon pack](../creating-an-icon-pack/), or added using the [`addIcon` API](../apis/tinymce.editor.ui.registry/#addIcon). |
| 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`](#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](../shortcuts/). |
| 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](../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

**Example**

```js
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`](../editor-command-identifiers/) command to apply and remove strikethrough formatting. This command toggles a specified format on and off, but only works for [formats](../content-formatting/#formats) already registered with the editor. In this example, `strikethrough` is the registered format.

### Basic toggle: 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`](../apis/tinymce.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:

- `formats` (String, required) — A comma-separated list of registered format names 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) — Restricts the listener to only match when the applied format variables are equal to `vars`.

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:

```js
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.on(eventName, callback)`](../apis/tinymce.editor/#on). To unbind an event listener use [`editor.off(eventName, callback)`](../apis/tinymce.editor/#off). 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)`.

> **Note:**
> - 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 () => {};`.
