primary-menu.php

I’ll build this as a powerful primary-menu.php template part for your RX theme, including accessibility, mobile menu markup, fallback menu, search, CTA buttons, schema attributes, hooks, and safe WordPress escaping.

My dear friend, paste this code into:

rx-theme/template-parts/header/primary-menu.php

This is an advanced RX Theme primary menu template with desktop menu, mobile menu button, off-canvas wrapper, fallback menu, search toggle, CTA button, accessibility labels, schema, hooks, filters, optional login/account link, WooCommerce cart badge support, and safe escaping.

<?php
/**
 * RX Theme - Primary Menu Template Part
 *
 * File: template-parts/header/primary-menu.php
 *
 * Features:
 * - WordPress primary menu support
 * - Accessible desktop navigation
 * - Mobile/off-canvas menu structure
 * - Fallback menu if no menu assigned
 * - Search toggle button
 * - CTA button support
 * - WooCommerce cart count support
 * - Login/account link support
 * - Schema.org SiteNavigationElement
 * - Hooks before/after menu
 * - Filterable menu settings
 * - Safe escaping and security-friendly output
 *
 * @package RX_Theme
 */

defined( 'ABSPATH' ) || exit;

/**
 * ------------------------------------------------------------
 * Menu configuration
 * ------------------------------------------------------------
 */

$rx_menu_location = apply_filters( 'rx_theme_primary_menu_location', 'primary' );

$rx_menu_id = apply_filters( 'rx_theme_primary_menu_id', 'rx-primary-menu' );

$rx_nav_id = apply_filters( 'rx_theme_primary_nav_id', 'rx-site-navigation' );

$rx_mobile_panel_id = apply_filters( 'rx_theme_mobile_menu_panel_id', 'rx-mobile-menu-panel' );

$rx_enable_mobile_menu = apply_filters( 'rx_theme_enable_mobile_menu', true );

$rx_enable_search_button = apply_filters( 'rx_theme_enable_header_search_button', true );

$rx_enable_cta_button = apply_filters( 'rx_theme_enable_header_cta_button', true );

$rx_enable_account_link = apply_filters( 'rx_theme_enable_header_account_link', true );

$rx_enable_cart_link = apply_filters( 'rx_theme_enable_header_cart_link', true );

$rx_cta_text = apply_filters( 'rx_theme_header_cta_text', __( 'Get Started', 'rx-theme' ) );

$rx_cta_url = apply_filters( 'rx_theme_header_cta_url', home_url( '/contact/' ) );

$rx_search_url = apply_filters( 'rx_theme_header_search_url', home_url( '/' ) );

$rx_account_url = is_user_logged_in() ? get_edit_profile_url( get_current_user_id() ) : wp_login_url();

$rx_account_text = is_user_logged_in()
	? apply_filters( 'rx_theme_header_account_logged_in_text', __( 'Account', 'rx-theme' ) )
	: apply_filters( 'rx_theme_header_account_logged_out_text', __( 'Login', 'rx-theme' ) );

/**
 * ------------------------------------------------------------
 * Helper: SVG icon output
 * ------------------------------------------------------------
 */

if ( ! function_exists( 'rx_theme_primary_menu_svg_icon' ) ) {
	/**
	 * Prints small inline SVG icons.
	 *
	 * @param string $icon Icon name.
	 * @return string
	 */
	function rx_theme_primary_menu_svg_icon( $icon = 'menu' ) {
		$icons = array(
			'menu' => '<svg class="rx-icon rx-icon-menu" width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false" role="img"><path d="M4 7h16M4 12h16M4 17h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',

			'close' => '<svg class="rx-icon rx-icon-close" width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false" role="img"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',

			'search' => '<svg class="rx-icon rx-icon-search" width="22" height="22" viewBox="0 0 24 24" aria-hidden="true" focusable="false" role="img"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="M16.5 16.5L21 21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',

			'user' => '<svg class="rx-icon rx-icon-user" width="22" height="22" viewBox="0 0 24 24" aria-hidden="true" focusable="false" role="img"><circle cx="12" cy="8" r="4" fill="none" stroke="currentColor" stroke-width="2"/><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',

			'cart' => '<svg class="rx-icon rx-icon-cart" width="22" height="22" viewBox="0 0 24 24" aria-hidden="true" focusable="false" role="img"><path d="M6 6h15l-2 9H8L6 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M6 6L5 3H2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="20" r="1.5" fill="currentColor"/><circle cx="18" cy="20" r="1.5" fill="currentColor"/></svg>',

			'arrow' => '<svg class="rx-icon rx-icon-arrow" width="14" height="14" viewBox="0 0 20 20" aria-hidden="true" focusable="false" role="img"><path d="M5 7l5 5 5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
		);

		$icon = isset( $icons[ $icon ] ) ? $icon : 'menu';

		return $icons[ $icon ];
	}
}

/**
 * ------------------------------------------------------------
 * Helper: fallback menu
 * ------------------------------------------------------------
 */

if ( ! function_exists( 'rx_theme_primary_menu_fallback' ) ) {
	/**
	 * Fallback menu when no WordPress menu is assigned.
	 *
	 * @return void
	 */
	function rx_theme_primary_menu_fallback() {
		?>
		<ul id="rx-primary-menu" class="rx-primary-menu rx-menu rx-menu-fallback">
			<li class="menu-item">
				<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
					<?php esc_html_e( 'Home', 'rx-theme' ); ?>
				</a>
			</li>

			<?php if ( get_option( 'page_for_posts' ) ) : ?>
				<li class="menu-item">
					<a href="<?php echo esc_url( get_permalink( get_option( 'page_for_posts' ) ) ); ?>">
						<?php esc_html_e( 'Blog', 'rx-theme' ); ?>
					</a>
				</li>
			<?php endif; ?>

			<?php
			$rx_pages = get_pages(
				array(
					'sort_column' => 'menu_order,post_title',
					'number'      => 5,
					'post_status' => 'publish',
				)
			);

			if ( ! empty( $rx_pages ) ) :
				foreach ( $rx_pages as $rx_page ) :
					?>
					<li class="menu-item">
						<a href="<?php echo esc_url( get_permalink( $rx_page->ID ) ); ?>">
							<?php echo esc_html( get_the_title( $rx_page->ID ) ); ?>
						</a>
					</li>
					<?php
				endforeach;
			endif;
			?>
		</ul>
		<?php
	}
}

/**
 * ------------------------------------------------------------
 * Helper: WooCommerce cart count
 * ------------------------------------------------------------
 */

$rx_cart_count = 0;

if ( class_exists( 'WooCommerce' ) && function_exists( 'WC' ) && WC()->cart ) {
	$rx_cart_count = absint( WC()->cart->get_cart_contents_count() );
}

?>

<div class="rx-primary-navigation-wrap" data-rx-component="primary-navigation">

	<?php
	/**
	 * Hook: before primary menu.
	 */
	do_action( 'rx_theme_before_primary_menu' );
	?>

	<nav
		id="<?php echo esc_attr( $rx_nav_id ); ?>"
		class="rx-site-navigation rx-primary-navigation"
		itemscope
		itemtype="https://schema.org/SiteNavigationElement"
		aria-label="<?php echo esc_attr_x( 'Primary menu', 'navigation aria label', 'rx-theme' ); ?>"
		data-rx-menu-location="<?php echo esc_attr( $rx_menu_location ); ?>"
	>

		<div class="rx-primary-navigation-inner">

			<?php if ( $rx_enable_mobile_menu ) : ?>
				<button
					type="button"
					class="rx-menu-toggle rx-mobile-menu-toggle"
					aria-controls="<?php echo esc_attr( $rx_mobile_panel_id ); ?>"
					aria-expanded="false"
					data-rx-menu-toggle
				>
					<span class="rx-menu-toggle-icon rx-menu-toggle-open">
						<?php echo rx_theme_primary_menu_svg_icon( 'menu' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</span>

					<span class="rx-menu-toggle-icon rx-menu-toggle-close">
						<?php echo rx_theme_primary_menu_svg_icon( 'close' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</span>

					<span class="screen-reader-text">
						<?php esc_html_e( 'Open main menu', 'rx-theme' ); ?>
					</span>
				</button>
			<?php endif; ?>

			<div class="rx-desktop-menu-wrap" data-rx-desktop-menu>
				<?php
				if ( has_nav_menu( $rx_menu_location ) ) {
					wp_nav_menu(
						apply_filters(
							'rx_theme_primary_menu_args',
							array(
								'theme_location'  => $rx_menu_location,
								'menu_id'         => $rx_menu_id,
								'menu_class'      => 'rx-primary-menu rx-menu rx-desktop-menu',
								'container'       => false,
								'fallback_cb'     => 'rx_theme_primary_menu_fallback',
								'depth'           => 4,
								'link_before'     => '<span class="rx-menu-link-text" itemprop="name">',
								'link_after'      => '</span>',
								'items_wrap'      => '<ul id="%1$s" class="%2$s" role="list">%3$s</ul>',
							)
						)
					);
				} else {
					rx_theme_primary_menu_fallback();
				}
				?>
			</div>

			<div class="rx-header-actions" data-rx-header-actions>

				<?php
				/**
				 * Hook: before header action buttons.
				 */
				do_action( 'rx_theme_before_header_actions' );
				?>

				<?php if ( $rx_enable_search_button ) : ?>
					<button
						type="button"
						class="rx-header-action rx-header-search-toggle"
						aria-label="<?php echo esc_attr_x( 'Open search form', 'button aria label', 'rx-theme' ); ?>"
						aria-expanded="false"
						aria-controls="rx-header-search-panel"
						data-rx-search-toggle
					>
						<?php echo rx_theme_primary_menu_svg_icon( 'search' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
						<span class="screen-reader-text">
							<?php esc_html_e( 'Search', 'rx-theme' ); ?>
						</span>
					</button>
				<?php endif; ?>

				<?php if ( $rx_enable_account_link ) : ?>
					<a
						class="rx-header-action rx-header-account-link"
						href="<?php echo esc_url( $rx_account_url ); ?>"
						aria-label="<?php echo esc_attr( $rx_account_text ); ?>"
					>
						<?php echo rx_theme_primary_menu_svg_icon( 'user' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
						<span class="rx-header-action-text">
							<?php echo esc_html( $rx_account_text ); ?>
						</span>
					</a>
				<?php endif; ?>

				<?php if ( $rx_enable_cart_link && class_exists( 'WooCommerce' ) && function_exists( 'wc_get_cart_url' ) ) : ?>
					<a
						class="rx-header-action rx-header-cart-link"
						href="<?php echo esc_url( wc_get_cart_url() ); ?>"
						aria-label="<?php echo esc_attr_x( 'View cart', 'cart link aria label', 'rx-theme' ); ?>"
					>
						<?php echo rx_theme_primary_menu_svg_icon( 'cart' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

						<span class="rx-cart-count" data-rx-cart-count>
							<?php echo esc_html( $rx_cart_count ); ?>
						</span>

						<span class="screen-reader-text">
							<?php esc_html_e( 'Cart', 'rx-theme' ); ?>
						</span>
					</a>
				<?php endif; ?>

				<?php if ( $rx_enable_cta_button && ! empty( $rx_cta_text ) && ! empty( $rx_cta_url ) ) : ?>
					<a
						class="rx-header-cta-button"
						href="<?php echo esc_url( $rx_cta_url ); ?>"
					>
						<?php echo esc_html( $rx_cta_text ); ?>
					</a>
				<?php endif; ?>

				<?php
				/**
				 * Hook: after header action buttons.
				 */
				do_action( 'rx_theme_after_header_actions' );
				?>

			</div>

		</div>
	</nav>

	<?php if ( $rx_enable_search_button ) : ?>
		<div
			id="rx-header-search-panel"
			class="rx-header-search-panel"
			hidden
			data-rx-search-panel
		>
			<div class="rx-header-search-inner">
				<form
					role="search"
					method="get"
					class="rx-header-search-form search-form"
					action="<?php echo esc_url( $rx_search_url ); ?>"
				>
					<label class="screen-reader-text" for="rx-header-search-field">
						<?php esc_html_e( 'Search for:', 'rx-theme' ); ?>
					</label>

					<input
						id="rx-header-search-field"
						type="search"
						class="rx-header-search-field search-field"
						placeholder="<?php echo esc_attr_x( 'Search here...', 'search placeholder', 'rx-theme' ); ?>"
						value="<?php echo esc_attr( get_search_query() ); ?>"
						name="s"
						autocomplete="off"
					/>

					<button type="submit" class="rx-header-search-submit search-submit">
						<?php echo rx_theme_primary_menu_svg_icon( 'search' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
						<span class="screen-reader-text">
							<?php esc_html_e( 'Submit search', 'rx-theme' ); ?>
						</span>
					</button>

					<button
						type="button"
						class="rx-header-search-close"
						aria-label="<?php echo esc_attr_x( 'Close search form', 'button aria label', 'rx-theme' ); ?>"
						data-rx-search-close
					>
						<?php echo rx_theme_primary_menu_svg_icon( 'close' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</button>
				</form>
			</div>
		</div>
	<?php endif; ?>

	<?php if ( $rx_enable_mobile_menu ) : ?>
		<div
			id="<?php echo esc_attr( $rx_mobile_panel_id ); ?>"
			class="rx-mobile-menu-panel"
			hidden
			data-rx-mobile-menu-panel
		>
			<div class="rx-mobile-menu-overlay" data-rx-mobile-menu-close></div>

			<aside
				class="rx-mobile-menu-drawer"
				aria-label="<?php echo esc_attr_x( 'Mobile menu', 'mobile menu aria label', 'rx-theme' ); ?>"
			>
				<div class="rx-mobile-menu-header">
					<strong class="rx-mobile-menu-title">
						<?php esc_html_e( 'Menu', 'rx-theme' ); ?>
					</strong>

					<button
						type="button"
						class="rx-mobile-menu-close"
						aria-label="<?php echo esc_attr_x( 'Close mobile menu', 'button aria label', 'rx-theme' ); ?>"
						data-rx-mobile-menu-close
					>
						<?php echo rx_theme_primary_menu_svg_icon( 'close' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</button>
				</div>

				<div class="rx-mobile-menu-body">
					<?php
					if ( has_nav_menu( $rx_menu_location ) ) {
						wp_nav_menu(
							apply_filters(
								'rx_theme_mobile_menu_args',
								array(
									'theme_location' => $rx_menu_location,
									'menu_id'        => 'rx-mobile-primary-menu',
									'menu_class'     => 'rx-mobile-primary-menu rx-menu rx-mobile-menu',
									'container'      => false,
									'fallback_cb'    => 'rx_theme_primary_menu_fallback',
									'depth'          => 4,
									'link_before'    => '<span class="rx-menu-link-text">',
									'link_after'     => '</span>',
									'items_wrap'     => '<ul id="%1$s" class="%2$s" role="list">%3$s</ul>',
								)
							)
						);
					} else {
						rx_theme_primary_menu_fallback();
					}
					?>
				</div>

				<div class="rx-mobile-menu-footer">

					<?php if ( $rx_enable_search_button ) : ?>
						<form
							role="search"
							method="get"
							class="rx-mobile-search-form search-form"
							action="<?php echo esc_url( home_url( '/' ) ); ?>"
						>
							<label class="screen-reader-text" for="rx-mobile-search-field">
								<?php esc_html_e( 'Search for:', 'rx-theme' ); ?>
							</label>

							<input
								id="rx-mobile-search-field"
								type="search"
								class="rx-mobile-search-field search-field"
								placeholder="<?php echo esc_attr_x( 'Search...', 'mobile search placeholder', 'rx-theme' ); ?>"
								value="<?php echo esc_attr( get_search_query() ); ?>"
								name="s"
							/>

							<button type="submit" class="rx-mobile-search-submit search-submit">
								<?php esc_html_e( 'Search', 'rx-theme' ); ?>
							</button>
						</form>
					<?php endif; ?>

					<?php if ( $rx_enable_cta_button && ! empty( $rx_cta_text ) && ! empty( $rx_cta_url ) ) : ?>
						<a
							class="rx-mobile-cta-button"
							href="<?php echo esc_url( $rx_cta_url ); ?>"
						>
							<?php echo esc_html( $rx_cta_text ); ?>
						</a>
					<?php endif; ?>

				</div>
			</aside>
		</div>
	<?php endif; ?>

	<?php
	/**
	 * Hook: after primary menu.
	 */
	do_action( 'rx_theme_after_primary_menu' );
	?>

</div>

Add this in your header.php where you want the menu to show:

<?php get_template_part( 'template-parts/header/primary-menu' ); ?>

Also make sure your theme registered the primary menu in inc/core/menus.php or functions.php:

<?php
/**
 * Register RX Theme menus.
 */
function rx_theme_register_menus() {
	register_nav_menus(
		array(
			'primary' => esc_html__( 'Primary Menu', 'rx-theme' ),
			'footer'  => esc_html__( 'Footer Menu', 'rx-theme' ),
			'mobile'  => esc_html__( 'Mobile Menu', 'rx-theme' ),
		)
	);
}
add_action( 'after_setup_theme', 'rx_theme_register_menus' );

For this file to work beautifully, later you should add JavaScript for these selectors:

[data-rx-menu-toggle]
[data-rx-mobile-menu-panel]
[data-rx-mobile-menu-close]
[data-rx-search-toggle]
[data-rx-search-panel]
[data-rx-search-close]

This template is safe to use now, but mobile open/close and search open/close need small JS behavior in your theme.

Leave a Reply

Your email address will not be published. Required fields are marked *