How to Create a Custom Widget in WordPress: Step-by-Step Guide

How to Create a Custom Widget in WordPress: Step-by-Step Guide

Did you know you can build your own custom WordPress widget? While themes and plugins ship with plenty of built-in widgets, sometimes you need something specific to your site — a social links block, a custom call-to-action, or a newsletter signup form that doesn’t fit any existing widget. Widgets let you add these non-content elements into a sidebar or any widget-ready area of your theme.

Common uses for a custom widget include search forms, banners, advertisements, sign-up forms, newsletter forms, and social media links — essentially any small, reusable piece of UI you want to drop into a sidebar or footer without hardcoding it into the theme template.

Building one isn’t complicated — it only requires basic knowledge of WordPress and PHP. This guide walks through the classic WP_Widget class approach step by step, including a real bug found in a common version of this code and how to fix it.

Note: Since WordPress 5.8, the Widgets screen itself is block-based — classic WP_Widget classes like the one below still work and appear as a "Legacy Widget" block inside the block-based widgets screen. If you’re starting a brand-new project today, also consider building a custom Gutenberg block instead, since blocks are the direction WordPress itself is moving. The WP_Widget approach below remains fully supported and is still the right choice for many existing themes and plugins.

Creating a Basic Custom Widget in WordPress

You have two options for where to place widget code: paste it directly into your theme’s functions.php file, or — the more durable choice — create a small dedicated plugin so the widget survives a theme switch. A WP_Widget subclass has four key methods you need to implement:

  • __construct() — defines the widget’s ID, display name, and description shown in the widget picker.
  • widget() — outputs the actual HTML shown on the front end.
  • update() — saves the widget’s settings when an admin updates it.
  • form() — renders the settings form shown in the admin widgets screen.
<?php

if( ! class_exists( 'socials_list_widget' ) ) 
{
    class socials_list_widget extends WP_Widget
    {

        // constructor function where you can define your widget’s ID, name, and description.
        function __construct() { 

            parent::__construct(
                'socials_list_widget',  // Widget ID
                esc_html__( 'Socials', 'text-domain' ),   // Widget Name
                array(
                    'description' => esc_html__( 'A widget that displays your socials_list', 'text-domain' ), 
                )
            );
     
        }

        //contains the output of the widget.
        function widget($args, $instance)
        {
            extract($args);

            $fb_checkbox_var = $instance['fb_checkbox_var'];
            $tt_checkbox_var = $instance['tt_checkbox_var'];
            $linkedin_checkbox_var = $instance['linkedin_checkbox_var'];
            $wt_checkbox_var = $instance['wt_checkbox_var'];
            $tg_checkbox_var = $instance['tg_checkbox_var'];

            ?>
            <div class="blog-post-aside">
                <ul class="blog-post-share">
                    <?php if ($fb_checkbox_var): ?>
                        <li class="blog-post-share__item">
                          <a href="<?php echo esc_url($fb_checkbox_var) ?>">
                            <img src="<?php echo get_template_directory_uri(); ?>/images/svg/icon-facebook.svg" alt="Facebook">
                          </a>
                        </li>
                    <?php endif; ?>
                    <?php if ($tt_checkbox_var): ?>
                        <li class="blog-post-share__item">
                          <a href="<?php echo esc_url($tt_checkbox_var) ?>">
                            <img src="<?php echo get_template_directory_uri(); ?>/images/svg/icon-twitter.svg" alt="Twitter">
                          </a>
                        </li>
                    <?php endif; ?>
                    <?php if ($linkedin_checkbox_var): ?>
                        <li class="blog-post-share__item">
                          <a href="<?php echo esc_url($linkedin_checkbox_var) ?>">
                            <img src="<?php echo get_template_directory_uri(); ?>/images/svg/icon-linkedin.svg" alt="LinkedIn">
                          </a>
                        </li>
                    <?php endif; ?>
                    <?php if ($wt_checkbox_var): ?>
                        <li class="blog-post-share__item">
                          <a href="<?php echo esc_url($wt_checkbox_var) ?>">
                            <img src="<?php echo get_template_directory_uri(); ?>/images/svg/icon-whatsapp.svg" alt="WhatsApp">
                          </a>
                        </li>
                    <?php endif; ?>
                    <?php if ($tg_checkbox_var): ?>
                        <li class="blog-post-share__item">
                          <a href="<?php echo esc_url($tg_checkbox_var) ?>">
                            <img src="<?php echo get_template_directory_uri(); ?>/images/svg/icon-telegram.svg" alt="Telegram">
                          </a>
                        </li>
                    <?php endif; ?>
                </ul>
            </div>

            <?php
            echo balanceTags($args['after_widget']);
        }


        //updates widget settings.
        function update($new_instance, $old_instance)
        {
            $instance = $old_instance;

            $instance['title'] = sanitize_text_field($new_instance['title']);
            $instance['fb_checkbox_var'] = esc_url_raw($new_instance['fb_checkbox_var']);
            $instance['tt_checkbox_var'] = esc_url_raw($new_instance['tt_checkbox_var']);
            $instance['linkedin_checkbox_var'] = esc_url_raw($new_instance['linkedin_checkbox_var']);
            $instance['wt_checkbox_var'] = esc_url_raw($new_instance['wt_checkbox_var']);
            $instance['tg_checkbox_var'] = esc_url_raw($new_instance['tg_checkbox_var']);

            return $instance;
        }


        //determines widget settings in the WordPress dashboard.
        function form($instance)
        {
            $defaults = array( 'title' => esc_html__('Social', 'cize-toolkit'), 'fb_checkbox_var' => esc_html__('https://facebook.com', 'cize-toolkit'), 'tt_checkbox_var' => esc_html__('https://twitter.com', 'cize-toolkit'),'linkedin_checkbox_var' => '','wt_checkbox_var'      => '','tg_checkbox_var'      => '');
            $instance = wp_parse_args( (array) $instance, $defaults );
            ?>
            <p>
                <label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('Title:', 'cize-toolkit'); ?></label>
                <input type="text" class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>"
                       name="<?php echo esc_attr($this->get_field_name('title')); ?>"
                       value="<?php echo esc_attr($instance['title']); ?>"/>
            </p>

            <p>
                <label for="<?php echo esc_attr($this->get_field_id('fb_checkbox_var')); ?>"><?php echo esc_html__('Facebook', 'cize-toolkit'); ?></label>
                <input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('fb_checkbox_var')); ?>"
                       name="<?php echo esc_attr($this->get_field_name('fb_checkbox_var')); ?>"
                       value="<?php echo esc_attr($instance['fb_checkbox_var']); ?>"/>
            </p>

            <p>
                <label for="<?php echo esc_attr($this->get_field_id('tt_checkbox_var')); ?>"><?php echo esc_html__('Twitter', 'cize-toolkit'); ?></label>
                <input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('tt_checkbox_var')); ?>"
                       name="<?php echo esc_attr($this->get_field_name('tt_checkbox_var')); ?>"
                       value="<?php echo esc_attr($instance['tt_checkbox_var']); ?>"/>
            </p>

            <p>
                <label for="<?php echo esc_attr($this->get_field_id('linkedin_checkbox_var')); ?>"><?php echo esc_html__('LinkedIn', 'cize-toolkit'); ?></label>
                <input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('linkedin_checkbox_var')); ?>"
                       name="<?php echo esc_attr($this->get_field_name('linkedin_checkbox_var')); ?>"
                       value="<?php echo esc_attr($instance['linkedin_checkbox_var']); ?>"/>
            </p>

            <p>
                <label for="<?php echo esc_attr($this->get_field_id('wt_checkbox_var')); ?>"><?php echo esc_html__('WhatsApp', 'cize-toolkit'); ?></label>
                <input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('wt_checkbox_var')); ?>"
                       name="<?php echo esc_attr($this->get_field_name('wt_checkbox_var')); ?>"
                       value="<?php echo esc_attr($instance['wt_checkbox_var']); ?>"/>
            </p>

            <p>
                <label for="<?php echo esc_attr($this->get_field_id('tg_checkbox_var')); ?>"><?php echo esc_html__('Telegram', 'cize-toolkit'); ?></label>
                <input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('tg_checkbox_var')); ?>"
                       name="<?php echo esc_attr($this->get_field_name('tg_checkbox_var')); ?>"
                       value="<?php echo esc_attr($instance['tg_checkbox_var']); ?>"/>
            </p>
            <?php
        }
    }
}

// Registering the widget class with WordPress
function wpwebguru_register_socials_widget() 
{
    register_widget( 'socials_list_widget' );
}
add_action( 'widgets_init', 'wpwebguru_register_socials_widget' );

Warning: A common mistake — including in earlier versions of this exact example — is naming your own wrapper function register_widget(). That’s a fatal error waiting to happen: register_widget() is already a real WordPress core function (the one that actually registers a widget class). Redeclaring it yourself triggers a "Cannot redeclare register_widget()" fatal PHP error and takes the whole site down. Always prefix your own function names with something unique to your plugin or theme, as shown above with wpwebguru_register_socials_widget().

Note: The corrected version above also switches update() from strip_tags()/raw storage to sanitize_text_field() and esc_url_raw(), and the form() fields now use esc_attr() instead of the unescaped balanceTags() output. Storing and echoing admin-submitted values without sanitizing on save and escaping on output is a common source of stored XSS in custom widgets — treat widget settings with the same care as any other user input, the same principle covered in our WordPress security checklist.

How WordPress Finds and Displays Your Widget

The add_action('widgets_init', ...) line is what tells WordPress your widget class exists at all — without it, the widget will never appear in the Appearance > Widgets screen no matter how correctly the class itself is written. Once registered, an admin drags it into any sidebar registered by the active theme via register_sidebar(), and the theme’s dynamic_sidebar() call in sidebar.php renders it on the front end automatically — you don’t need to call your widget class directly anywhere in your templates.

Wrapping Up

A custom widget is a small, self-contained way to add reusable, admin-configurable UI to any sidebar without touching template files every time you need a small change. If you’re starting fresh, weigh this classic WP_Widget approach against building a custom Gutenberg block instead — blocks give the same reusability with a more modern editing experience. And whichever approach you choose, sanitize on save and escape on output every time, exactly as shown in the corrected code above; it’s the same habit that matters in a custom REST API endpoint or anywhere else user-supplied data flows through your site. For the full official reference on every method and parameter available, see the WP_Widget class reference.

Leave a Reply

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

One reply on “How to Create a Custom Widget in WordPress: Step-by-Step Guide”

  • laywk
    ·
    April 21, 2021 at 12:19 am

    It’s really helped me

The link has been Copied to clipboard!