Quicktags Button Generator

A button for the Text tab of the classic editor, hooked where QTags is guaranteed to exist, with the block editor caveat stated up front.

Enable JavaScript to customise; default output below.

Must be unique. Reusing an ID that core already has, such as strong or link, replaces that button.

Kept short: the toolbar is narrow and wraps badly. Core uses lower case for most of its buttons.

Only used when the button wraps.

Becomes Alt+Shift+key. Leave it empty rather than colliding with a browser or screen reader shortcut.

Core buttons sit between 10 and 140, so a number above that puts yours at the end.

Live preview my_plugin-quicktags.php
<?php
/**
 * Adds the "callout" button to the Text tab of the classic editor.
 *
 * Quicktags is the toolbar above the Text tab. It is not the block editor and it is not the
 * Visual tab: this button appears in exactly one place, and on a block-editor site that place
 * does not exist unless the classic editor is in use somewhere.
 */

/**
 * Prints the button registration in the admin footer.
 *
 * QTags.addButton has to run after quicktags.js, and the footer of the editor screen is the
 * hook that guarantees that without adding a dependency.
 */
function my_plugin_quicktags_button() {
	$screen = get_current_screen();

	// Only the post editor. Without this the script is printed on every admin page.
	if ( ! $screen || 'post' !== $screen->base ) {
		return;
	}

	// wp_enqueue_editor() has run by now on any screen with an editor; if quicktags is not
	// loaded, QTags is undefined and calling addButton would be a JavaScript error.
	?>
	<script>
	if ( 'undefined' !== typeof QTags ) {
		QTags.addButton(
			'my_plugin_callout',
			'callout',
			'\u003Cdiv class=\"callout\">',
			'\u003C/div>',
			'',
			'Wrap the selection in a callout',
			200
		);
	}
	</script>
	<?php
}
add_action( 'admin_print_footer_scripts', 'my_plugin_quicktags_button' );

Output is valid and updates as you type.

Quicktags is the toolbar above the Text tab of the classic editor. Not the Visual tab, and not the block editor.

That is worth saying first, because most tutorials for this do not, and a button added to a site running the block editor appears nowhere at all. If your editing experience is blocks, the equivalent is a format or a block, which is a different job entirely.

Where Quicktags does apply, the API is one function: QTags.addButton. The only real trap is when to call it, since the script has to run after quicktags.js has defined QTags, and that is what the footer hook below is for.

How to use

  1. Give the button a unique ID and a short label.
  2. Set the text it inserts. Wrapping buttons take an opening and closing string and toggle around the selection.
  3. Drop the code in a small plugin or the theme’s functions.php.

Example

function my_plugin_quicktags_button() {
	$screen = get_current_screen();

	// Only the post editor. Without this the script is printed on every admin page.
	if ( ! $screen || 'post' !== $screen->base ) {
		return;
	}

	?>
	<script>
	if ( 'undefined' !== typeof QTags ) {
		QTags.addButton(
			'my_plugin_callout',
			'callout',
			'<div class=\"callout\">',
			'</div>',
			'',
			'Wrap the selection in a callout',
			200
		);
	}
	</script>
	<?php
}
add_action( 'admin_print_footer_scripts', 'my_plugin_quicktags_button' );

The < characters come out as < because the script is printed inside a page: an unescaped </script> in any of those strings would end the block early. That is the escaping doing its job, and the browser reads it as the character it stands for.

Pitfalls

It does nothing in the block editor. There is no Quicktags toolbar there. A site using the classic editor plugin, or a classic metabox, still has one, and that is where this appears.

Guard against QTags being undefined. On an admin screen without an editor the object does not exist, and calling addButton on it throws, which stops every later script on the page. The screen check and the typeof check together are why this is safe to hook in the footer.

IDs collide silently. Reusing an ID core already uses, such as strong, em, link or more, replaces that button rather than adding yours. Prefix it.

Access keys collide too. They become Alt+Shift+key and the browser and screen readers already claim several. Leaving the access key empty is usually right.

What the button inserts still has to survive saving. For anyone below administrator, wp_kses_post strips tags and attributes not on the allowed list, so a button that inserts a <div data-x> may produce markup that vanishes on save. Test as the role that will use it.

Priority is sparse, not ordinal. Core buttons sit between 10 and 140, so 200 puts yours at the end. Two buttons with the same priority order unpredictably.

Enabling it on the comment form gives commenters a toolbar. That is a deliberate choice about the kind of comments you want, and kses strips most of what the buttons could insert anyway.

Compatibility

Everything runs in the browser: nothing is uploaded and nothing is stored.

QTags.addButton has been in WordPress since 3.3 and its signature has not changed: id, display, arg1, arg2, access_key, title, priority, instance. This generator leaves instance off, so the button appears on every Quicktags instance on the page, which is what you want in the post editor.

The hook is admin_print_footer_scripts, which runs late enough that quicktags.js is loaded, combined with a get_current_screen() check so the script is not printed on unrelated admin pages. Enqueuing a separate file with wp-editor as a dependency is the tidier approach for more than one button; for one it is more moving parts than the problem needs.

Every value is escaped for its context: the label and inserted text through JavaScript string escaping, which turns < into < so that </script> cannot terminate the block, and the docblock text through comment escaping so */ cannot close it. The hostile fixture in the test suite contains both, plus ?>, and the generated file still passes php -l and keeps the same token structure as the default one.

Frequently asked questions

Does this work with the block editor?
No. The block editor has no Quicktags toolbar. You want a block, a block style, or a rich-text format depending on what the markup is for.
Where do I put the code?
A one-file plugin is the right home, because it is editor behaviour rather than presentation and it should survive a theme change. functions.php works if the theme is yours and permanent.
Why is my button not appearing?
Either the site is on the block editor, or the code is hooked too early for QTags to exist, or the screen check is excluding the page you are looking at. Check the browser console for an error on QTags is not defined.
Can it insert a shortcode?
Yes, and that is one of the better uses: a wrapping button that puts [callout] and [/callout] around the selection. The shortcode generator on this site writes the other half.
How do I remove a core button?
QTags.deleteButton( 'id' ), called the same way. Removing link or img tends to annoy people more than it helps, so it is worth asking why first.

From the people who built this tool

WP Adminify

The WordPress admin, rebuilt: a dashboard worth looking at, menu and column control, a real file manager and the login page your client sees.

See WP Adminify Free version on WordPress.org

Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.