Widgets & Navigation Menus
register_sidebar and register_nav_menus declare the slots a site owner fills; dynamic_sidebar and wp_nav_menu render them.
register_sidebar( array|string $args = array() ): stringWidget 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
<?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.
<?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.
<?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.
<?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