WordPress Custom Widgets – Mastering Advanced WordPress Development Series

WordPress Hooks and Filters – Mastering Advanced WordPress Development Series
WordPress Custom Widgets – Mastering Advanced WordPress Development Series

Learn how to create custom WordPress widgets from scratch with this step-by-step guide. Perfect for developers looking to add custom functionality to their site.

You’ve built the site, and now the client wants one small box in the sidebar that shows something specific. None of the built-in widgets do it. That’s the moment you stop reaching for a plugin and write your own.

This post is part of our Mastering Advanced WordPress Development series, and here we’re building a custom widget from an empty class to a working box on the page. We’ll cover what a widget actually is, write one the right way, and register it so WordPress can see it.

One honest note before we start, because it changes how you’ll test this. Since WordPress 5.8 (July 2021), the Appearance > Widgets screen is block-based by default. The old drag-and-drop widget screen is gone unless you install the official Classic Widgets plugin. The WP_Widget class we’re using here still works. Your widget just shows up inside a “Legacy Widget” block instead of the old list. If you want the classic screen back, that plugin restores it.

What are Widgets in WordPress?

A widget is a small, self-contained block of functionality you drop into a widget area, usually a sidebar or footer. It’s for content that doesn’t belong in the main body of a page: a search box, a calendar, recent posts, social links, that kind of thing.

WordPress ships with a handful of these and manages them in the admin. Custom widgets exist for everything the defaults don’t cover.

Creating a Custom Widget

A widget lives in PHP, so put this code where PHP belongs: a plugin, or your theme’s functions.php. A small standalone plugin is the cleaner choice, because then the widget survives a theme switch.

Step 1: Defining the Custom Widget Class

Every custom widget extends WordPress’s WP_Widget class. That parent gives you the plumbing; you override four methods to fill in the behavior.

PHP
<?php
/**
 * Custom Widget class to create a custom widget in WordPress.
 */
class Custom_Widget extends WP_Widget {
   /**
    * Sets up the widget ID, name, and description.
    */
   function __construct() {
      parent::__construct(
         'custom_widget', // Widget ID
         esc_html( 'Custom Widget', 'text_domain' ), // Widget name
         array( 'description' => esc_html__( 'A custom widget', 'text_domain' ), ) // Widget description
      );
   }
   /**
    * Outputs the content of the widget on the front-end.
    *
    * @param array $args Display arguments including 'before_title', 'after_title', 'before_widget', and 'after_widget'.
    * @param array $instance The settings for the particular instance of the widget.
    */
   public function widget( $args, $instance ) {
      // Widget output
   }
   /**
    * Outputs the options form on the admin side.
    *
    * @param array $instance The widget options.
    * @return void
    */
   public function form( $instance ) {
      // Widget form
   }
   /**
    * Handles updating the widget instance settings.
    *
    * @param array $new_instance New settings for this instance as input by the user.
    * @param array $old_instance Old settings for this instance.
    * @return array Updated settings to save.
    */
   public function update( $new_instance, $old_instance ) {
      // Save widget options
   }
}

Here’s what each of the four methods does.

The constructor runs when the widget is created and sets its ID, name, and description. One correction on the code above: the widget name should use esc_html__() (the translation function with two underscores), not esc_html(). WordPress’s esc_html() only takes one argument and won’t translate a string. It’s a common typo, and worth fixing in your own copy.

The widget() method runs on the front end. This is where you output the widget’s HTML.

The form() method draws the settings fields in the admin.

The update() method runs when those settings are saved, and it’s where you sanitize and store them.

Step 2: Creating the Widget HTML

Now fill in widget() so the thing actually renders.

PHP
<?php
/**
 * Outputs the widget content on the front-end.
 *
 * @param array $args Display arguments.
 * @param array $instance The settings for the particular instance of the widget.
 */
public function widget( $args, $instance ) {
   echo $args['before_widget'];
   if ( ! empty( $instance['title'] ) ) {
      echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
   }
   // Widget content
   echo '<p>Hello World!</p>';
   echo $args['after_widget'];
}

The before_widget, before_title, after_title, and after_widget values come from the widget area itself. Echoing them means your widget inherits the theme’s wrapper markup and heading styles instead of fighting them.

widget() receives two arguments. $args carries that positional markup; $instance carries the saved settings, like the title. Here we print the title if one’s set, then a plain “Hello World!” so you can confirm it’s alive. Swap that line for your real output.

Step 3: Creating the Widget Form

The front end works. Now give the user something to configure. That’s the form() method.

PHP
/**
 * Outputs the options form on the admin side.
 *
 * @param array $instance The widget options.
 * @return void
 */
public function form( $instance ) {
   $title = ! empty( $instance['title'] ) ? $instance['title'] : esc_html__( 'New title', 'text_domain' );
?>
<p>
   <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_attr_e( 'Title:', 'text_domain' ); ?></label>
   <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
</p>
<?php
}

One text field for the title, prefilled with whatever’s already saved. The get_field_id() and get_field_name() helpers matter more than they look: they generate unique IDs and names per widget instance, so two copies of the same widget don’t collide. Everything going into an attribute runs through esc_attr() on the way out.

Step 4: Saving the Widget Options

When someone hits save, update() runs. Its job is to clean the incoming values and return what should be stored.

PHP
<?php
/**
 * Handles updating the widget instance settings.
 *
 * @param array $new_instance New settings for this instance as input by the user.
 * @param array $old_instance Old settings for this instance.
 * @return array Updated settings to save.
 */
public function update( $new_instance, $old_instance ) {
   $instance = array();
   $instance['title'] = ( ! empty( $new_instance['title'] ) ) ? sanitize_text_field( $new_instance['title'] ) : '';
   return $instance;
}

Build a fresh $instance array, run the title through sanitize_text_field(), and return it. Never trust $new_instance raw; sanitize every field before it hits the database. Whatever you return here is exactly what gets saved.

Step 5: Registering and Adding the Widget

Here’s the step the class alone can’t do, and it’s the one most tutorials skip. A widget class does nothing until you register it. WordPress won’t find Custom_Widget on its own. You hand it over on the widgets_init hook.

PHP
<?php
/**
 * Registers the Custom_Widget with WordPress.
 */
function register_custom_widget() {
   register_widget( 'Custom_Widget' );
}
add_action( 'widgets_init', 'register_custom_widget' );

register_widget() takes the class name as a string and hooks in on widgets_init. Skip this and your widget simply never shows up, no error, no warning. It’s the most common reason a “broken” widget isn’t broken at all.

With that in place, go to Appearance > Widgets. On WordPress 5.8 and later, add a Legacy Widget block and pick your widget from the list; on the classic screen (or with Classic Widgets installed) you’ll drag it into a widget area the old way. Set the title, save, and load the front end to confirm it renders.

Conclusion

That’s the full loop: extend WP_Widget, fill in widget(), form(), and update(), then register the class on widgets_init so WordPress can see it. The “Hello World!” output is a placeholder. The real value is the pattern, and once it clicks you can build a widget for almost anything: a feed, a form, a custom nav block, whatever the sidebar needs.

Two things to carry forward. First, sanitize on the way in and escape on the way out, every time, no exceptions. Second, know where WordPress is headed: block-based widgets are the default now, and for new work it’s worth learning to build widgets as blocks. The classic WP_Widget approach here still ships and still works, which is exactly why it’s a solid place to learn the mechanics before you move on to blocks.

Leave a Comment

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


Scroll to Top