Widgets & Navigation Menus

register_sidebar and register_nav_menus declare the slots a site owner fills; dynamic_sidebar and wp_nav_menu render them.

Syntaxregister_sidebar( array|string $args = array() ): string

Widget areas and navigation menus are the two ways a site owner rearranges content without touching code. In both cases code declares a named slot and a template renders whatever the user put in it.

Widget areas

register_sidebar( array|string $args = array() ): string runs on widgets_init and returns the sidebar id. before_widget/after_widget and before_title/after_title are the wrapper markup printed around every widget; before_sidebar/after_sidebar were added in WordPress 5.6 and show_in_rest in 5.9. register_sidebars( 3, $args ) is shorthand for three register_sidebar() calls sharing $args, but it does not number them for you: each name is passed through sprintf(), so put a %d placeholder in it yourself, and a supplied id is only given a -2, -3 suffix when it collides with one already registered. Render with dynamic_sidebar( 'acme-footer' ), guarded by is_active_sidebar().

Menu locations

A location is a named slot; the menu assigned to it is user data. Register locations on after_setup_theme. The singular form is core's own one-line wrapper:

register_nav_menu( 'primary', 'Primary menu' );
// core's implementation, verbatim:
register_nav_menus( array( 'primary' => 'Primary menu' ) );

Then render with wp_nav_menu( array( 'theme_location' => 'primary' ) ).

Block widgets

WordPress 5.8 replaced the Widgets screen with a block editor. Registered sidebars still exist and still render through dynamic_sidebar(); classic widgets appear wrapped in a Legacy Widget block. Opt out with remove_theme_support( 'widgets-block-editor' ) from a theme, or add_filter( 'use_widgets_block_editor', '__return_false' ) from a plugin. Block themes use template parts and the Navigation block instead of sidebars.

Example

Example · php
<?php
defined( 'ABSPATH' ) || exit;

/**
 * Widget areas are registered on 'widgets_init'.
 * A plugin may register them just as a theme does.
 */
add_action( 'widgets_init', static function (): void {
	register_sidebar(
		array(
			'name'          => __( 'Events Sidebar', 'acme-events' ),
			'id'            => 'acme-events-sidebar', // what dynamic_sidebar() takes
			'description'   => __( 'Shown beside single event pages.', 'acme-events' ),
			// %1$s is the widget id, %2$s the widget's CSS classes. Core substitutes both.
			'before_widget' => '<section id="%1$s" class="widget %2$s">',
			'after_widget'  => '</section>',
			'before_title'  => '<h2 class="widget-title">',
			'after_title'   => '</h2>',
			// Added in WordPress 5.6.
			'before_sidebar' => '<div class="acme-events-sidebar">',
			'after_sidebar'  => '</div>',
			// Added in WordPress 5.9; exposes the area at /wp-json/wp/v2/sidebars.
			'show_in_rest'   => true,
		)
	);
} );

/**
 * Menu locations are registered on 'after_setup_theme'.
 */
add_action( 'after_setup_theme', static function (): void {
	register_nav_menus(
		array(
			'acme-events-primary'   => __( 'Events primary menu', 'acme-events' ),
			'acme-events-secondary' => __( 'Events secondary menu', 'acme-events' ),
		)
	);
} );

// In a template file:
// is_active_sidebar() is false when the area exists but holds no widgets,
// so the wrapper markup is not printed around nothing.
if ( is_active_sidebar( 'acme-events-sidebar' ) ) {
	echo '<aside class="sidebar">';
	dynamic_sidebar( 'acme-events-sidebar' );
	echo '</aside>';
}

When to use it

  • Adding a widget area to a plugin-provided template so site owners can drop content beside it.
  • Registering a header and footer menu location in a classic theme.
  • Preventing a brand-new site's header from listing every published page before a menu is assigned.
  • Turning the block widgets editor off on a site whose widgets predate WordPress 5.8.

More examples

Rendering a menu safely

The fallback_cb => false and has_nav_menu() pairing is the fix for the most reported navigation bug: a fresh install shows every page in the header because no menu has been assigned yet. echo => false is what you need whenever the menu goes into a shortcode or a returned string rather than straight to the browser.

Example · php
<?php
defined( 'ABSPATH' ) || exit;

// has_nav_menu( string $location ): bool - true only when a menu is assigned
// to the location in Appearance > Menus.
if ( has_nav_menu( 'acme-events-primary' ) ) {
	wp_nav_menu(
		array(
			'theme_location'  => 'acme-events-primary',
			'container'       => 'nav',            // 'div' by default; false for none
			'container_class' => 'acme-nav',
			'container_aria_label' => __( 'Events navigation', 'acme-events' ), // WP 5.5
			'menu_class'      => 'acme-nav__list',
			'depth'           => 2,                // 0 = unlimited
			// %1$s = menu_id, %2$s = menu_class, %3$s = the <li> items.
			'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
			'fallback_cb'     => false,            // never fall back to wp_page_menu
		)
	);
}

// 'echo' => false returns the markup instead of printing it, which is what a
// shortcode or a REST callback needs. Returns false if there is no menu.
$markup = wp_nav_menu(
	array(
		'theme_location' => 'acme-events-primary',
		'echo'           => false,
		'fallback_cb'    => false,
	)
);

if ( is_string( $markup ) ) {
	return $markup;
}

Registering a classic widget from a plugin

Classic widgets still work in WordPress 6.8 — the block editor shows them inside a Legacy Widget block. Write one when the plugin has to support sites that are still on classic widgets or on the Customizer; otherwise a block is the better target for new work.

Example · php
<?php
defined( 'ABSPATH' ) || exit;

class Acme_Upcoming_Events_Widget extends WP_Widget {

	public function __construct() {
		parent::__construct(
			'acme_upcoming_events',                        // $id_base
			__( 'Upcoming Events', 'acme-events' ),        // $name
			array( 'description' => __( 'A list of the next events.', 'acme-events' ) )
		);
	}

	/** Front-end output. $args holds before_widget etc from register_sidebar(). */
	public function widget( $args, $instance ) {
		echo $args['before_widget'];

		if ( ! empty( $instance['title'] ) ) {
			echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'];
		}

		echo do_shortcode( '[acme_events limit="' . absint( $instance['limit'] ?? 3 ) . '"]' );
		echo $args['after_widget'];
	}

	/** The admin form. */
	public function form( $instance ) {
		printf(
			'<p><label for="%1$s">%2$s</label><input class="widefat" id="%1$s" name="%3$s" type="text" value="%4$s" /></p>',
			esc_attr( $this->get_field_id( 'title' ) ),
			esc_html__( 'Title:', 'acme-events' ),
			esc_attr( $this->get_field_name( 'title' ) ),
			esc_attr( $instance['title'] ?? '' )
		);
	}

	/** Sanitise before saving. Return false to cancel the save. */
	public function update( $new_instance, $old_instance ) {
		return array(
			'title' => sanitize_text_field( $new_instance['title'] ?? '' ),
			'limit' => absint( $new_instance['limit'] ?? 3 ),
		);
	}
}

add_action( 'widgets_init', static function (): void {
	register_widget( 'Acme_Upcoming_Events_Widget' );
} );

Restoring the classic widgets editor

Needed when a site depends on classic widgets that render badly through the Legacy Widget wrapper, or when a client has been trained on the old screen. Prefer the filter from a plugin: remove_theme_support() is a statement about the theme, and it will surprise whoever swaps the theme next.

Example · php
<?php
defined( 'ABSPATH' ) || exit;

// From a theme: remove the support core adds by default in WordPress 5.8+.
// Must run after core registers it, so after_setup_theme is the place.
add_action( 'after_setup_theme', static function (): void {
	remove_theme_support( 'widgets-block-editor' );
} );

// From a plugin or mu-plugin, where you should not speak for the theme:
// use_widgets_block_editor is a bool filter, so __return_false is the
// whole callback - it is core's shorthand for function () { return false; }.
add_filter( 'use_widgets_block_editor', '__return_false' );

// Equivalent longhand, if you need to make the decision conditional:
add_filter( 'use_widgets_block_editor', static function ( bool $use ): bool {
	// Keep blocks for administrators, classic widgets for everyone else.
	return current_user_can( 'manage_options' ) ? $use : false;
} );

Discussion

  • Be the first to comment on this lesson.