12th Feb '26
/
0 comments

Custom keyboard shortcuts in Bricks builder

This article provides the code for setting up a few custom keyboard shortcuts in Bricks editor to make the workflow a little faster.

KeyAction
sInsert Section
cInsert Container
bInsert Block
dInsert Div
tInsert Text (Basic)
hInsert Heading
iInsert Image
rInsert Rich Text
lInsert Text Link
wWrap selected element(s) with the configured wrap element (default: Block)
Alt+HToggle :hover pseudo-class

These make composing extremely fast in Bricks.

Also, changes the default Bricks behavior of being able to rename elements in the structure panel with a single click (poor UX in my opinion) to double click.

Note:

  • Single-key shortcuts (s, c, b, d, t, h, i, r, l, w) only fire when no modifier keys are held down.
  • All shortcuts are disabled when typing in form fields, CodeMirror editors, or when the command palette is open
  • Shortcuts work in both the main builder panel and the canvas iframe
  • w respects Bricks’ builderWrapElement setting (same as CMD/CTRL+Shift+P)

Step 1

Edit child theme‘s functions.php.

Replace

add_action( 'wp_enqueue_scripts', function() {
	// Enqueue your files on the canvas & frontend, not the builder panel. Otherwise custom CSS might affect builder)
	if ( ! bricks_is_builder_main() ) {
		wp_enqueue_style( 'bricks-child', get_stylesheet_uri(), ['bricks-frontend'], filemtime( get_stylesheet_directory() . '/style.css' ) );
	}
} );

with

add_action( 'wp_enqueue_scripts', function() {
	// Enqueue your files on the canvas & frontend, not the builder panel. Otherwise custom CSS might affect builder)
	if ( ! bricks_is_builder_main() ) {
		wp_enqueue_style( 'bricks-child', get_stylesheet_uri(), ['bricks-frontend'], filemtime( get_stylesheet_directory() . '/style.css' ) );
	}
	
	// Enqueue builder keyboard shortcuts (Alt+H to toggle :hover, single key shortcuts)
	if ( bricks_is_builder_main() ) {
		wp_enqueue_script(
			'bl-builder-shortcuts',
			get_stylesheet_directory_uri() . '/js/builder-shortcuts.js',
			array( 'bricks-builder' ),
			filemtime( get_stylesheet_directory() . '/js/builder-shortcuts.js' ),
			true,
		);
	}
} );

Step 2

Create a folder named js in the child theme directory.

Inside the js folder, create a file named builder-shortcuts.js having this code:

JavaScript code
( function () {
	/**
	 * Single-key shortcuts to insert elements.
	 *
	 * s = Section, c = Container, b = Block, d = Div,
	 * t = Text (Basic), h = Heading, i = Image,
	 * r = Rich Text, l = Text Link,
	 * w = Wrap selected element(s) with configured wrap element
	 */
	var ELEMENT_SHORTCUTS = {
		KeyS: 'section',
		KeyC: 'container',
		KeyB: 'block',
		KeyD: 'div',
		KeyT: 'text-basic',
		KeyH: 'heading',
		KeyI: 'image',
		KeyR: 'text',
		KeyL: 'text-link',
	};

	/* -----------------------------------------------------------------------
	 * Vue reference caching (lazy-init).
	 *
	 * The Bricks Vue app, its globalProperties and $_state are stable
	 * references for the entire builder session. We cache them on first
	 * access instead of querying the DOM on every keypress.
	 * ----------------------------------------------------------------------- */
	var _gp = null;
	var _state = null;

	/**
	 * Return the cached Vue global properties, initialising on first call.
	 */
	function gp() {
		if ( ! _gp ) {
			var app = window.top.document.querySelector( '.brx-body' );

			if ( app && app.__vue_app__ ) {
				_gp = app.__vue_app__.config.globalProperties;
				_state = _gp.$_state;
			}
		}

		return _gp;
	}

	/* -----------------------------------------------------------------------
	 * Element lookup helper.
	 *
	 * Prefers Bricks' built-in $_getDynamicElementById (likely O(1)) and
	 * falls back to an array search on $_dynamicElements.value.
	 * ----------------------------------------------------------------------- */

	/**
	 * Look up an element by its ID using the fastest available method.
	 *
	 * @param {string} id Element ID.
	 * @return {Object|null} The element object or null.
	 */
	function getElementById( id ) {
		var props = gp();

		if ( ! props ) {
			return null;
		}

		// Bricks provides $_getDynamicElementById for fast lookups.
		if ( typeof props.$_getDynamicElementById === 'function' ) {
			return props.$_getDynamicElementById( id );
		}

		// Fallback: linear search through the elements array.
		var elements = props.$_dynamicElements && props.$_dynamicElements.value;

		return elements
			? elements.find( function ( el ) { return el.id === id; } )
			: null;
	}

	/* -----------------------------------------------------------------------
	 * Editable-target guard.
	 * ----------------------------------------------------------------------- */

	/**
	 * Check if the event target is an editable field where typing should be allowed.
	 * Readonly inputs inside the structure panel are NOT considered editable (the user
	 * is just selecting an element, not renaming it).
	 */
	function isEditableTarget( e ) {
		var tag = e.target.tagName;

		if ( tag === 'INPUT' ) {
			// Allow shortcuts on readonly structure-panel label inputs.
			if ( e.target.readOnly && e.target.closest( '.structure-item .title' ) ) {
				return false;
			}

			return true;
		}

		if ( tag === 'TEXTAREA' || e.target.isContentEditable ) {
			return true;
		}

		// CodeMirror editor
		if ( e.target.closest && e.target.closest( '.CodeMirror' ) ) {
			return true;
		}

		return false;
	}

	/* -----------------------------------------------------------------------
	 * Shortcut handlers — action-only (guard logic lives in the dispatcher).
	 * ----------------------------------------------------------------------- */

	/**
	 * Toggle :hover pseudo-class.
	 */
	function toggleHover( e ) {
		e.preventDefault();

		if ( ! _state ) {
			return;
		}

		_state.pseudoClassActive = _state.pseudoClassActive === ':hover' ? undefined : ':hover';
		_state.activeSelector = undefined;
		_state.showElementClasses = false;

		// Ensure pseudo-classes panel is visible (same as watcher behavior).
		if ( _state.pseudoClassActive ) {
			localStorage.setItem( 'brx_show_pseudo_classes', 'true' );
		}
	}

	/**
	 * Walk up the element tree from the active element to find its ancestor section.
	 * Returns the section element object, or null if not found.
	 */
	function findAncestorSection() {
		if ( ! _state || ! _state.activeElement ) {
			return null;
		}

		var current = _state.activeElement;

		while ( current ) {
			if ( current.name === 'section' ) {
				return current;
			}

			if ( ! current.parent ) {
				return null;
			}

			current = getElementById( current.parent );
		}

		return null;
	}

	/**
	 * Insert an element by name via the matching ELEMENT_SHORTCUTS entry.
	 */
	function insertElement( e ) {
		var elementName = ELEMENT_SHORTCUTS[ e.code ];

		if ( ! elementName ) {
			return;
		}

		e.preventDefault();

		var props = gp();
		var el = props.$_createElement( { name: elementName } );

		// When inserting a section, place it immediately after the current section
		// (or the section that contains the active element). Bricks' addNewElement
		// overwrites the index for sections, so we temporarily set the active element
		// to the ancestor section and enable insertAfter.
		if ( elementName === 'section' ) {
			var section = findAncestorSection();

			if ( section ) {
				_state.activeId = section.id;
				_state.insertAfter = true;

				props.$_addNewElement( { element: el } );

				_state.insertAfter = false;

				return;
			}
		}

		props.$_addNewElement( { element: el } );
	}

	/**
	 * Wrap selected element(s) with the configured wrap element (default: Block).
	 * Respects the same builderWrapElement setting as Bricks' CMD/CTRL+Shift+P.
	 */
	function wrapWithBlock( e ) {
		if ( ! _state.activeElement ) {
			return;
		}

		e.preventDefault();

		var wrapType = ( window.bricksData && window.bricksData.builderWrapElement ) || 'block';

		gp().$_wrapInNestable( null, wrapType );
	}

	/* -----------------------------------------------------------------------
	 * Unified keyboard dispatcher.
	 *
	 * A single keydown listener per document replaces the previous three.
	 * Guard logic (modifier keys, editable targets, command palette) is
	 * checked once, then the appropriate handler is called.
	 * ----------------------------------------------------------------------- */

	function handleKeydown( e ) {
		// Never intercept typing in form fields.
		if ( isEditableTarget( e ) ) {
			return;
		}

		// Alt+H — toggle :hover (allows Alt modifier, blocks others).
		if ( e.altKey && e.code === 'KeyH' && ! e.ctrlKey && ! e.metaKey && ! e.shiftKey && ! e.repeat ) {
			gp();
			toggleHover( e );
			return;
		}

		// Everything below requires NO modifier keys.
		if ( e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.repeat ) {
			return;
		}

		if ( ! gp() || ! _state ) {
			return;
		}

		// Don't act when the command palette is open.
		if ( _state.showCommandPalette ) {
			return;
		}

		if ( e.code === 'KeyW' ) {
			wrapWithBlock( e );
			return;
		}

		if ( ELEMENT_SHORTCUTS[ e.code ] ) {
			insertElement( e );
		}
	}

	/**
	 * Attach the unified keydown listener to a document (main or iframe).
	 */
	function attachListeners( doc ) {
		doc.addEventListener( 'keydown', handleKeydown );
	}

	/* -----------------------------------------------------------------------
	 * Structure panel: require double-click to edit element labels.
	 *
	 * Bricks' Vue click handler sets a reactive `readonly` variable to false
	 * on every single click, and Vue then removes the DOM readonly attribute
	 * in its render cycle.
	 *
	 * Strategy: a MutationObserver always reverts readonly removal
	 * immediately. When we detect a double-click (via the dblclick event),
	 * we temporarily pause the observer, explicitly remove readonly, and
	 * resume the observer after a short delay.
	 * ----------------------------------------------------------------------- */

	function patchStructureLabelClick() {
		function applyPatch( panel ) {
			var paused = false;

			// MutationObserver: always revert readonly removal while not paused.
			var attrObserver = new MutationObserver( function ( mutations ) {
				if ( paused ) {
					return;
				}

				for ( var i = 0; i < mutations.length; i++ ) {
					var m = mutations[ i ];

					if (
						m.type === 'attributes' &&
						m.attributeName === 'readonly' &&
						m.target.tagName === 'INPUT' &&
						! m.target.hasAttribute( 'readonly' ) &&
						m.target.closest( '.structure-item .title' )
					) {
						m.target.setAttribute( 'readonly', '' );
						m.target.readOnly = true;
					}
				}
			} );

			attrObserver.observe( panel, {
				attributes: true,
				attributeFilter: [ 'readonly' ],
				subtree: true,
			} );

			// On double-click: pause the observer, explicitly enter edit mode.
			panel.addEventListener( 'dblclick', function ( e ) {
				var input = e.target.closest( '.structure-item .title input[type="text"]' );

				if ( ! input ) {
					return;
				}

				// Pause observer so our changes aren't reverted.
				paused = true;

				// Remove readonly to enter edit mode.
				input.removeAttribute( 'readonly' );
				input.readOnly = false;
				input.select();

				// Resume observer after Vue has settled.
				setTimeout( function () {
					paused = false;
				}, 200 );
			}, true );
		}

		// The structure panel may not exist yet. Watch for it.
		var existing = document.getElementById( 'bricks-structure' );

		if ( existing ) {
			applyPatch( existing );
			return;
		}

		var observer = new MutationObserver( function () {
			var panel = document.getElementById( 'bricks-structure' );

			if ( panel ) {
				applyPatch( panel );
				observer.disconnect();
			}
		} );

		observer.observe( document.body, { childList: true, subtree: true } );
	}

	/* -----------------------------------------------------------------------
	 * Auto-expand new Sections and select their Container.
	 *
	 * Wraps $_addNewElement so that when a Section is added (via any method —
	 * keyboard shortcut, + button, command palette) the new Section is
	 * expanded in the structure panel and its auto-created Container becomes
	 * the active element.
	 *
	 * Instead of relying on scrollToElementId (which only works when Bricks'
	 * structureAutoSync is enabled), we click the DOM toggle arrow directly,
	 * matching the approach used by Advanced Themer.
	 * ----------------------------------------------------------------------- */

	function patchAddNewElementForSections() {
		var props = gp();

		if ( ! props || ! props.$_addNewElement ) {
			return;
		}

		var original = props.$_addNewElement;

		// Idempotency guard — avoid double-wrapping if the script runs twice.
		if ( original.__bl_patched ) {
			return;
		}

		props.$_addNewElement = function () {
			var result = original.apply( this, arguments );

			// Only act when a section was just created.
			if ( ! result || result.name !== 'section' ) {
				return result;
			}

			var sectionId = result.id;

			// Wait one tick for Vue / Xp() to finish creating nestable children.
			setTimeout( function () {
				var section = getElementById( sectionId );

				if ( ! section || ! section.children || ! section.children.length ) {
					return;
				}

				// First child is the auto-created Container.
				var containerId = section.children[ 0 ];

				// Expand the section in the structure panel by clicking its toggle
				// arrow. This works regardless of the structureAutoSync setting.
				var toggle = document.querySelector(
					'#bricks-structure .element[data-id="' + sectionId + '"] .toggle'
				);

				if ( toggle && toggle.dataset.name === 'arrow-right' ) {
					toggle.click();
				}

				// Select the container.
				_state.activeId = containerId;
				_state.activePanel = 'element';
			}, 0 );

			return result;
		};

		props.$_addNewElement.__bl_patched = true;
	}

	/* -----------------------------------------------------------------------
	 * Initialisation.
	 * ----------------------------------------------------------------------- */

	// Listen on main document.
	attachListeners( document );

	// Also listen inside the builder canvas iframe (where focus usually is).
	var iframe = document.getElementById( 'bricks-builder-iframe' );

	if ( iframe ) {
		iframe.addEventListener( 'load', function () {
			try {
				attachListeners( iframe.contentDocument );
			} catch ( err ) {
				// Same-origin policy — should not happen on local dev.
			}
		} );

		// Iframe may already be loaded.
		try {
			if ( iframe.contentDocument && iframe.contentDocument.readyState === 'complete' ) {
				attachListeners( iframe.contentDocument );
			}
		} catch ( err ) {
			// Ignore.
		}
	}

	// Apply structure panel label patch.
	patchStructureLabelClick();

	// Patch addNewElement to auto-select container inside new sections.
	// Retries until Vue is fully mounted and the global property is available.
	function initPatches() {
		if ( ! gp() || ! gp().$_addNewElement ) {
			setTimeout( initPatches, 100 );
			return;
		}

		patchAddNewElementForSections();
	}

	initPatches();
} )();

Reload the builder.

Do not forget the default shortcuts in Bricks.

These four are especially useful with the above setup:

Credit for the JavaScript code: Claude.

Get access to all 612 Bricks code tutorials with BricksLabs Pro

Leave the first comment

 

Related Tutorials..

How to Change Bricks Preloader Background Color

How to Change Bricks Preloader Background Color

Working on a dark-themed site like our friend Storm and getting blinded by the light background of Bricks' preloader screen of the editor? Here's how…
Categories:
Keyboard shortcuts for adding elements in Bricks builder

Keyboard shortcuts for adding elements in Bricks builder

Advanced Themer is going to add a handy feature where pressing Cmd/Ctrl + Shift will show numbers in the right sidebar element shortcuts. Typing a…
Collapse All button for Elements in Bricks Editor

Collapse All button for Elements in Bricks Editor

Bricks' editor shows the various buttons to add elements in the left side grouped into different categories. While it is possible to collapse them all…
Categories:
Left Structure Panel in Bricks Editor

Left Structure Panel in Bricks Editor

Eric shared some CSS code in the Bricks Facebook group to move the structure panel in between the element panel and canvas here. I think…