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.
One reply on “How to Create a Custom Widget in WordPress: Step-by-Step Guide”
It’s really helped me