Skip to content

Instantly share code, notes, and snippets.

@gbyat
Created July 1, 2026 09:29
Show Gist options
  • Select an option

  • Save gbyat/d9dbe28c1cae4cb9038b49b3bfa24000 to your computer and use it in GitHub Desktop.

Select an option

Save gbyat/d9dbe28c1cae4cb9038b49b3bfa24000 to your computer and use it in GitHub Desktop.
WordPress Navigation Breakpoint for Menublocks
( function ( wp ) {
if (
! wp ||
! wp.hooks ||
! wp.compose ||
! wp.element ||
! wp.components ||
! wp.i18n
) {
return;
}
var addFilter = wp.hooks.addFilter;
var createHigherOrderComponent = wp.compose.createHigherOrderComponent;
var createElement = wp.element.createElement;
var Fragment = wp.element.Fragment;
var editorApi = wp.blockEditor || wp.editor;
if ( ! editorApi || ! editorApi.InspectorControls ) {
return;
}
var InspectorControls = editorApi.InspectorControls;
var PanelBody = wp.components.PanelBody;
var TextControl = wp.components.TextControl;
var SelectControl = wp.components.SelectControl;
var NumberControl = wp.components.__experimentalNumberControl;
var __ = wp.i18n.__;
var ATTRIBUTE_NAME = 'webentwicklerinBreakpoint';
var ALIGN_ATTRIBUTE_NAME = 'webentwicklerinMobileIconAlign';
var DEFAULT_BREAKPOINT = 600;
var DEFAULT_ALIGNMENT = 'inherit';
var EDITOR_PREVIEW_STYLE_ID = 'webentwicklerin-navigation-alignment-preview';
function getOverlayMenuMode( attributes ) {
if ( ! attributes || typeof attributes.overlayMenu === 'undefined' ) {
return 'mobile';
}
return attributes.overlayMenu;
}
function sanitizeValue( value ) {
var parsed = parseInt( value, 10 );
if ( Number.isNaN( parsed ) ) {
return DEFAULT_BREAKPOINT;
}
if ( parsed < 320 ) {
return 320;
}
if ( parsed > 1920 ) {
return 1920;
}
return parsed;
}
function addNavigationBreakpointAttribute( settings, blockName ) {
if ( blockName !== 'core/navigation' ) {
return settings;
}
settings.attributes = Object.assign( {}, settings.attributes, {
webentwicklerinBreakpoint: {
type: 'number',
default: DEFAULT_BREAKPOINT,
},
webentwicklerinMobileIconAlign: {
type: 'string',
default: DEFAULT_ALIGNMENT,
},
} );
return settings;
}
function sanitizeAlignment( value ) {
if ( value === 'inherit' || value === 'left' || value === 'center' || value === 'right' ) {
return value;
}
return DEFAULT_ALIGNMENT;
}
function ensureEditorPreviewStyles() {
if ( ! document || document.getElementById( EDITOR_PREVIEW_STYLE_ID ) ) {
return;
}
var style = document.createElement( 'style' );
style.id = EDITOR_PREVIEW_STYLE_ID;
style.textContent =
'.block-editor-block-list__block.webentwicklerin-mobile-icon-align-left nav.wp-block-navigation,' +
'.block-editor-block-list__block.webentwicklerin-mobile-icon-align-center nav.wp-block-navigation,' +
'.block-editor-block-list__block.webentwicklerin-mobile-icon-align-right nav.wp-block-navigation{width:100%!important;}' +
'.block-editor-block-list__block.webentwicklerin-mobile-icon-align-left nav.wp-block-navigation .wp-block-navigation__responsive-container-open{margin-left:0!important;margin-right:auto!important;}' +
'.block-editor-block-list__block.webentwicklerin-mobile-icon-align-center nav.wp-block-navigation .wp-block-navigation__responsive-container-open{margin-left:auto!important;margin-right:auto!important;}' +
'.block-editor-block-list__block.webentwicklerin-mobile-icon-align-right nav.wp-block-navigation .wp-block-navigation__responsive-container-open{margin-left:auto!important;margin-right:0!important;}';
document.head.appendChild( style );
}
function createBreakpointControl( value, onChange ) {
var label = __( 'Responsive navigation breakpoint (px)', 'we-navigation-breakpoint' );
var help = __( 'Defines when the menu switches to the mobile toggle.', 'we-navigation-breakpoint' );
if ( NumberControl ) {
return createElement( NumberControl, {
label: label,
help: help,
value: value,
min: 320,
max: 1920,
step: 1,
onChange: onChange,
} );
}
return createElement( TextControl, {
label: label,
help: help,
type: 'number',
value: value,
min: 320,
max: 1920,
step: 1,
onChange: onChange,
} );
}
function createAlignmentControl( value, onChange ) {
return createElement( SelectControl, {
label: __( 'Mobile menu icon alignment', 'we-navigation-breakpoint' ),
help: __( 'Aligns the visible mobile menu icon independently from the menu alignment.', 'we-navigation-breakpoint' ),
value: value,
options: [
{
label: __( 'Inherit (native behavior)', 'we-navigation-breakpoint' ),
value: 'inherit',
},
{
label: __( 'Left', 'we-navigation-breakpoint' ),
value: 'left',
},
{
label: __( 'Center', 'we-navigation-breakpoint' ),
value: 'center',
},
{
label: __( 'Right', 'we-navigation-breakpoint' ),
value: 'right',
},
],
onChange: onChange,
} );
}
var withNavigationBreakpointControl = createHigherOrderComponent(
function ( BlockEdit ) {
return function ( props ) {
if ( props.name !== 'core/navigation' ) {
return createElement( BlockEdit, props );
}
var currentValue = props.attributes[ ATTRIBUTE_NAME ];
var safeValue = Number.isFinite( currentValue ) ? currentValue : DEFAULT_BREAKPOINT;
var currentAlignment = props.attributes[ ALIGN_ATTRIBUTE_NAME ];
var safeAlignment = sanitizeAlignment( currentAlignment );
var overlayMode = getOverlayMenuMode( props.attributes );
var hasMobileOverlay = overlayMode === 'mobile';
var hasToggleIcon = overlayMode === 'mobile' || overlayMode === 'always';
function onChangeBreakpoint( nextValue ) {
props.setAttributes( {
webentwicklerinBreakpoint: sanitizeValue( nextValue ),
} );
}
function onChangeAlignment( nextValue ) {
props.setAttributes( {
webentwicklerinMobileIconAlign: sanitizeAlignment( nextValue ),
} );
}
return createElement(
Fragment,
null,
createElement( BlockEdit, props ),
createElement(
InspectorControls,
null,
createElement(
PanelBody,
{
title: __( 'Responsive Settings', 'we-navigation-breakpoint' ),
initialOpen: true,
},
hasMobileOverlay
? createBreakpointControl( safeValue, onChangeBreakpoint )
: null,
hasToggleIcon
? createAlignmentControl( safeAlignment, onChangeAlignment )
: createElement( 'p', null, __( 'Available when Overlay menu is set to Mobile or Always.', 'we-navigation-breakpoint' ) )
)
)
);
};
},
'withNavigationBreakpointControl'
);
var withNavigationAlignmentPreview = createHigherOrderComponent(
function ( BlockListBlock ) {
return function ( props ) {
if ( props.name !== 'core/navigation' ) {
return createElement( BlockListBlock, props );
}
ensureEditorPreviewStyles();
var attributes = props.attributes || {};
var overlayMode = getOverlayMenuMode( attributes );
var hasToggleIcon = overlayMode === 'mobile' || overlayMode === 'always';
var alignment = sanitizeAlignment( attributes[ ALIGN_ATTRIBUTE_NAME ] );
var alignmentClass = hasToggleIcon && alignment !== 'inherit'
? 'webentwicklerin-mobile-icon-align-' + alignment
: '';
var className = props.className ? props.className + ' ' + alignmentClass : alignmentClass;
return createElement(
BlockListBlock,
Object.assign( {}, props, {
className: className,
} )
);
};
},
'withNavigationAlignmentPreview'
);
addFilter(
'blocks.registerBlockType',
'we-navigation-breakpoint/navigation-breakpoint-attribute',
addNavigationBreakpointAttribute
);
addFilter(
'editor.BlockEdit',
'we-navigation-breakpoint/navigation-breakpoint-control',
withNavigationBreakpointControl
);
addFilter(
'editor.BlockListBlock',
'we-navigation-breakpoint/navigation-alignment-preview',
withNavigationAlignmentPreview
);
} )( window.wp );
<?php
/**
* Plugin Name: WE Navigation Breakpoint
* Description: Adds a configurable responsive breakpoint and mobile icon alignment for the Navigation block.
* Version: 1.0.1
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: webentwicklerin, Gabriele Laesser
* Author URI: https://webentwicklerin.at
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: we-navigation-breakpoint
*
* @package Webentwicklerin\NavigationBreakpoint
*/
namespace Webentwicklerin\NavigationBreakpoint;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
const VERSION = '1.0.1';
const SCRIPT_HANDLE = 'we-navigation-breakpoint-control';
/**
* Registers server-side attributes for the Navigation block.
*
* @return void
*/
function register_navigation_attributes() {
if ( ! class_exists( '\WP_Block_Type_Registry' ) ) {
return;
}
$registry_class = '\WP_Block_Type_Registry';
$navigation_block = $registry_class::get_instance()->get_registered( 'core/navigation' );
if ( ! $navigation_block ) {
return;
}
$navigation_block->attributes['webentwicklerinBreakpoint'] = array(
'type' => 'number',
'default' => 600,
);
$navigation_block->attributes['webentwicklerinMobileIconAlign'] = array(
'type' => 'string',
'default' => 'inherit',
);
}
/**
* Enqueues block editor assets for Navigation controls.
*
* @return void
*/
function enqueue_block_editor_assets() {
$script_path = plugin_dir_path( __FILE__ ) . 'assets/js/navigation-breakpoint-control.js';
$script_url = plugin_dir_url( __FILE__ ) . 'assets/js/navigation-breakpoint-control.js';
if ( ! file_exists( $script_path ) ) {
return;
}
wp_enqueue_script(
SCRIPT_HANDLE,
$script_url,
array( 'wp-blocks', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-element', 'wp-hooks', 'wp-i18n' ),
(string) filemtime( $script_path ),
true
);
wp_set_script_translations(
SCRIPT_HANDLE,
'we-navigation-breakpoint',
plugin_dir_path( __FILE__ ) . 'languages'
);
}
/**
* Enqueues editor assets for editors that do not trigger enqueue_block_editor_assets.
*
* @return void
*/
function enqueue_block_assets_fallback() {
if ( ! is_admin() ) {
return;
}
enqueue_block_editor_assets();
}
/**
* Returns a safe breakpoint value.
*
* @param mixed $value Breakpoint value.
* @return int
*/
function sanitize_breakpoint( $value ) {
$breakpoint = absint( $value );
if ( $breakpoint < 320 ) {
return 320;
}
if ( $breakpoint > 1920 ) {
return 1920;
}
return $breakpoint;
}
/**
* Returns a safe mobile icon alignment value.
*
* @param mixed $value Alignment value.
* @return string
*/
function sanitize_mobile_icon_alignment( $value ) {
$allowed_values = array( 'inherit', 'left', 'center', 'right' );
if ( is_string( $value ) && in_array( $value, $allowed_values, true ) ) {
return $value;
}
return 'inherit';
}
/**
* Returns CSS declarations for mobile icon alignment.
*
* @param string $alignment Alignment value.
* @return string
*/
function get_mobile_icon_alignment_css( $alignment ) {
if ( 'left' === $alignment ) {
return 'margin-left:0!important;margin-right:auto!important;';
}
if ( 'center' === $alignment ) {
return 'margin-left:auto!important;margin-right:auto!important;';
}
if ( 'right' !== $alignment ) {
return '';
}
return 'margin-left:auto!important;margin-right:0!important;';
}
/**
* Returns the overlay menu mode used by the Navigation block.
*
* @param array $attributes Block attributes.
* @return string
*/
function get_overlay_menu_mode( $attributes ) {
if ( empty( $attributes['overlayMenu'] ) ) {
// Core defaults to mobile overlay when the setting is not explicitly saved.
return 'mobile';
}
return (string) $attributes['overlayMenu'];
}
/**
* Adds custom responsive breakpoint and icon alignment CSS to Navigation block output.
*
* @param string $block_content Rendered block HTML.
* @param array $block Parsed block data.
* @return string
*/
function render_navigation_with_breakpoint( $block_content, $block ) {
if ( empty( $block['blockName'] ) || 'core/navigation' !== $block['blockName'] ) {
return $block_content;
}
$attributes = isset( $block['attrs'] ) ? $block['attrs'] : array();
$has_custom_breakpoint = isset( $attributes['webentwicklerinBreakpoint'] );
$has_custom_alignment = isset( $attributes['webentwicklerinMobileIconAlign'] );
$alignment = sanitize_mobile_icon_alignment(
$has_custom_alignment ? $attributes['webentwicklerinMobileIconAlign'] : 'inherit'
);
$has_effective_alignment = $has_custom_alignment && 'inherit' !== $alignment;
$overlay_mode = get_overlay_menu_mode( $attributes );
$has_mobile_overlay = 'mobile' === $overlay_mode;
$has_toggle_icon = $has_mobile_overlay || 'always' === $overlay_mode;
$breakpoint = sanitize_breakpoint(
$has_custom_breakpoint ? $attributes['webentwicklerinBreakpoint'] : 600
);
// Keep core behavior untouched when no custom value is saved.
if ( ( ! $has_mobile_overlay && ! $has_toggle_icon ) || ( ! $has_custom_breakpoint && ! $has_effective_alignment ) ) {
return $block_content;
}
if ( ! class_exists( '\WP_HTML_Tag_Processor' ) ) {
return $block_content;
}
$processor_class = '\WP_HTML_Tag_Processor';
$processor = new $processor_class( $block_content );
if ( ! $processor->next_tag( 'nav' ) ) {
return $block_content;
}
$nav_id = $processor->get_attribute( 'id' );
if ( empty( $nav_id ) ) {
$nav_id = 'nav-' . wp_unique_id();
$processor->set_attribute( 'id', $nav_id );
}
$updated_content = $processor->get_updated_html();
$escaped_nav_id = esc_attr( $nav_id );
$css_rules = '';
if ( $has_mobile_overlay && $has_custom_breakpoint ) {
$max_breakpoint = max( 0, $breakpoint - 1 );
$css_rules .= sprintf(
'@media (max-width:%1$dpx){#%2$s .wp-block-navigation__responsive-container-open:not(.always-shown){display:flex!important;}#%2$s .wp-block-navigation__responsive-container:not(.is-menu-open):not(.hidden-by-default){display:none!important;}}@media (min-width:%3$dpx){#%2$s .wp-block-navigation__responsive-container-open:not(.always-shown){display:none!important;}#%2$s .wp-block-navigation__responsive-container:not(.is-menu-open):not(.hidden-by-default){display:block!important;}}',
$max_breakpoint,
$escaped_nav_id,
$breakpoint
);
}
if ( $has_toggle_icon && $has_effective_alignment ) {
$button_css = get_mobile_icon_alignment_css( $alignment );
$alignment_rule = sprintf(
'#%1$s{width:100%%!important;}#%1$s .wp-block-navigation__responsive-container-open{%2$s}',
$escaped_nav_id,
$button_css
);
if ( $has_mobile_overlay ) {
$max_breakpoint = max( 0, $breakpoint - 1 );
$css_rules .= sprintf(
'@media (max-width:%1$dpx){%2$s}',
$max_breakpoint,
$alignment_rule
);
} else {
$css_rules .= $alignment_rule;
}
}
return '<style>' . $css_rules . '</style>' . $updated_content;
}
add_action( 'init', __NAMESPACE__ . '\register_navigation_attributes', 100 );
add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_block_editor_assets' );
add_action( 'enqueue_block_assets', __NAMESPACE__ . '\enqueue_block_assets_fallback', 20 );
add_filter( 'render_block', __NAMESPACE__ . '\render_navigation_with_breakpoint', 10, 2 );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment