Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 089 / 100

Widgets and sidebars: WordPress best practices explained

Classic widget areas and block widgets.

Expona Admin6 min read

Widgets and sidebars: WordPress best practices explained

WordPress widget systems have evolved significantly over the past few years, yet many teams still treat widgets and sidebars as an afterthought in their theme development. Understanding the distinction between classic widget registration, block-based widget areas, and modern block themes will help you build more maintainable and future-proof projects. This guide walks through the practical decisions we make when architecting widget support across different WordPress themes.

Register sidebars — register_sidebar in classic themes

Registering sidebars is the foundational step in classic theme development. When you call register_sidebar(), you're telling WordPress where on your pages widget instances can live. This is still the primary approach for themes that haven't fully transitioned to the block editor.

In a typical e-commerce theme, you might have a sidebar in your product template, a footer widget area for social links, and perhaps a secondary sidebar for related posts. Each of these needs explicit registration during theme setup.

php
function my_theme_widgets_init() {
  register_sidebar( array(
    'name'          => 'Primary Sidebar',
    'id'            => 'primary-sidebar',
    'description'   => 'Main sidebar for single posts and pages',
    'before_widget' => '<div id="%1$s" class="widget %2$s">',
    'after_widget'  => '</div>',
    'before_title'  => '<h3 class="widget-title">',
    'after_title'   => '</h3>',
  ) );

  register_sidebar( array(
    'name'          => 'Footer Widgets',
    'id'            => 'footer-widgets',
    'description'   => 'Widgets displayed in the footer area',
    'before_widget' => '<div id="%1$s" class="widget %2$s">',
    'after_widget'  => '</div>',
    'before_title'  => '<h4 class="widget-title">',
    'after_title'   => '</h4>',
  ) );
}

add_action( 'widgets_init', 'my_theme_widgets_init' );

Notice the before_widget and after_widget parameters. These wrap each widget instance with markup, allowing you to apply consistent styling and structure. The %1$s and %2$s placeholders receive the widget's HTML ID and CSS classes respectively.

Then, in your template files, you render the sidebar using dynamic_sidebar():

php
<aside class="sidebar">
  <?php
  if ( is_active_sidebar( 'primary-sidebar' ) ) {
    dynamic_sidebar( 'primary-sidebar' );
  }
  ?>
</aside>

We always check is_active_sidebar() first to avoid rendering empty containers. This pattern scales well across multi-widget themes because each sidebar maintains its own registered ID and display logic.

Block widgets — Widget areas now accept blocks

Starting with WordPress 5.8, widget areas support blocks in addition to legacy widget plugins. This represents a significant shift: site builders no longer need separate block and widget interfaces.

A block widget area still uses register_sidebar(), but WordPress automatically makes it compatible with the block editor. When you open the Widgets screen, you'll see the block inserter rather than the classic widget selection panel.

The key difference for developers is that block widgets generate markup directly from the block's save() method, not from the before_widget and after_widget wrappers. You need to decide: do you want widget areas that accept only blocks, only legacy widgets, or both?

To force a widget area to use blocks exclusively, add 'show_in_rest' => true when registering:

php
register_sidebar( array(
  'name'              => 'Hero Section',
  'id'                => 'hero-widgets',
  'description'       => 'Block-based hero content area',
  'before_widget'     => '',
  'after_widget'      => '',
  'before_title'      => '',
  'after_title'       => '',
  'show_in_rest'      => true,
) );

With empty wrapper strings and show_in_rest => true, your widget area becomes a "block template area." Site builders can insert any block—Paragraph, Image, Columns, custom blocks—and each block's output appears as registered.

In template code, you still use dynamic_sidebar(), and WordPress renders the blocks correctly:

php
<div class="hero">
  <?php dynamic_sidebar( 'hero-widgets' ); ?>
</div>

This approach works well for landing pages and special template sections where you want content flexibility without custom post types.

Custom widgets — Prefer blocks over WP_Widget classes

When you need specialized functionality—say, a "Featured Products" widget for an e-commerce site—the old approach was to extend WP_Widget. However, we now recommend building custom blocks instead, which integrate seamlessly with both the block editor and widget areas.

Why favor blocks? They're more composable, easier to test, and automatically supported in all block-aware contexts. A block you register works in posts, pages, patterns, and widget areas without extra configuration.

Here's a minimal custom block that displays recent posts:

php
register_block_type(
  'my-theme/recent-posts',
  array(
    'render_callback' => 'render_recent_posts_block',
    'attributes'      => array(
      'count' => array(
        'type'    => 'number',
        'default' => 5,
      ),
    ),
  )
);

function render_recent_posts_block( $attributes ) {
  $args = array(
    'posts_per_page' => $attributes['count'],
    'post_type'      => 'post',
  );
  
  $posts = get_posts( $args );
  
  if ( empty( $posts ) ) {
    return '<p>No posts found.</p>';
  }
  
  $html = '<div class="recent-posts">';
  foreach ( $posts as $post ) {
    $html .= sprintf(
      '<article><a href="%s">%s</a></article>',
      esc_url( get_permalink( $post ) ),
      esc_html( $post->post_title )
    );
  }
  $html .= '</div>';
  
  return $html;
}

This block can be placed in any widget area. The render_callback runs server-side, so you have full access to WordPress APIs. No need to maintain a separate WP_Widget class.

If you inherit projects with legacy WP_Widget implementations, they still work—WordPress maintains backward compatibility. But for new work, blocks are the better path forward.

Block themes — Template parts replace sidebars

Full-site editing (FSE) themes use a different architecture. Instead of registering sidebars in functions.php, you define template parts in your theme's parts/ directory.

A block theme doesn't typically have a traditional sidebar. Instead, sidebars become optional, and layout is managed entirely through block templates. The product page template, for instance, might include a custom block area using the wp:template-part block to pull in a sidebar-like structure.

Here's a simplified FSE template that achieves a similar effect to a classic sidebar layout:

php
<!-- wp:group {"layout":{"type":"flex"}} -->
<div class="wp-block-group">

  <!-- wp:group {"flex":"1"} -->
  <div class="wp-block-group">
    <!-- wp:post-content /-->
  </div>
  <!-- /wp:group -->

  <!-- wp:template-part {"slug":"sidebar","theme":"my-theme"} /-->

</div>
<!-- /wp:group -->

The wp:template-part block references a file at parts/sidebar.html, which contains your sidebar markup and blocks. This replaces the classic dynamic_sidebar() pattern entirely.

Block themes still support widget areas if needed—you register them the same way—but the primary layout tool is the template editor and template parts. If your team is building a modern FSE theme, lean into this paradigm rather than mixing classic sidebars with block templates.

Common mistakes

Not checking widget area existence. Always use is_active_sidebar() before calling dynamic_sidebar(). This prevents markup artifacts when no widgets are assigned.

Mixing wrapper logic in blocks and sidebars. If you're registering a block-only widget area, set wrapper strings to empty. If widgets still use the classic system, keep wrappers. Don't do both—it creates duplicate markup.

Forgetting to escape output in custom blocks. Use esc_url(), esc_html(), and wp_kses_post() when rendering block markup. Unescaped output is a security vulnerability.

Assuming all sites support blocks. Older WordPress versions and some deployments disable the block editor entirely. Test your widget setup on the specific WordPress versions your clients use.

Overcomplicating sidebar registration. Each register_sidebar() call should have a clear, unique ID and purpose. Avoid generic names like "sidebar" and "widgets"—use semantic IDs like "product-sidebar" or "footer-subscribe".

Key takeaways

  • Use register_sidebar() in classic themes to define widget areas. Always set meaningful IDs and appropriate before/after wrapper markup.
  • Block-based widget areas are registered the same way but render blocks instead of legacy widgets. Add 'show_in_rest' => true to enable block support.
  • Build custom functionality as blocks rather than extending WP_Widget. Blocks are more flexible, composable, and future-proof.
  • In block themes, use template parts and the block editor's layout system instead of traditional sidebars. FSE handles positioning and responsiveness through block layout controls.
  • Always check widget area existence with is_active_sidebar() before rendering. Escape all output in custom blocks.
  • Test widget behavior across the WordPress versions your team supports. Document which themes use classic sidebars and which use FSE patterns.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles