Learn how to build a custom WordPress widget using the WP_Widget class. This step-by-step guide covers backend logic, admin forms, front-end output, and best practices for security and performance.
A widget is a small, self-contained piece of content you can drop into a sidebar, a footer, or any widget area your theme exposes. Contact details, featured posts, recent comments: the stuff you want on every page without editing a template each time. Here we’ll build one from scratch, a Contact Info widget, using the classic WordPress Widgets API.
One honest caveat before we start. Since WordPress 5.8, released July 20, 2021, widget areas use the block editor by default, so most people now add blocks instead of writing a widget class. The WP_Widget approach in this guide still works, and it’s the right call when you’re packaging reusable functionality inside a plugin or theme. Just know going in that it’s the legacy path. If all you want is to place some content, a block is simpler.
Table of Contents
- Introduction: What is a WordPress Widget?
- The Foundation: Extending the
WP_WidgetClass - Step 1: Setting Up the Widget (
__construct) - Step 2: Building the Admin Settings Form (
form()) - Step 3: Saving and Sanitizing Settings (
update()) - Step 4: Displaying the Widget on the Front-End (
widget()) - Step 5: Registering Your Widget with WordPress (
widgets_initHook) - Step 6: Adding Basic CSS Styles
- Crucial Best Practices: Security, Maintainability & More
- Bonus Tip: Making Your Widget Translatable
- Conclusion
The Foundation: Extending the WP_Widget Class
Every custom widget starts by extending the WP_Widget base class. That’s what hands you the four methods that do the real work: one to set up the widget, one to draw its admin form, one to save its settings, and one to render it on the front end. You override those; WordPress wires up the rest.
Step 1: Setting Up the Widget (__construct)
The __construct method names your widget. You pass a unique base ID, the label that shows up in the admin, and a short description. That name and description are what people read when they browse the Widgets screen, so make them clear.
<?php
/**
* Class for a custom "Contact Info" widget.
*/
class Contact_Info_Widget extends WP_Widget {
/**
* Constructor.
*/
public function __construct() {
parent::__construct(
'contact_info_widget', // Base ID
__( 'Contact Info Widget', 'your-text-domain' ), // Name
array( 'description' => __( 'A widget to display contact information.', 'your-text-domain' ) )
);
}
// Other methods will be defined below...
}Step 2: Building the Admin Settings Form (form())
The form() method draws the settings form admins see. You build the input fields here, one per setting, and pre-fill each with its saved value so editing an existing widget shows what’s already there. For our Contact Info widget that’s a title, an address, and a phone number.
/**
* Outputs the options form on admin.
*
* @param array $instance Current settings.
*/
public function form( $instance ) {
// Retrieve stored values or set defaults.
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Our Contact Info', 'your-text-domain' );
$address = ! empty( $instance['address'] ) ? $instance['address'] : '';
$phone = ! empty( $instance['phone'] ) ? $instance['phone'] : '';
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
<?php esc_html_e( 'Title:', 'your-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>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'address' ) ); ?>">
<?php esc_html_e( 'Address:', 'your-text-domain' ); ?>
</label>
<textarea class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'address' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'address' ) ); ?>"><?php echo esc_textarea( $address ); ?></textarea>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'phone' ) ); ?>">
<?php esc_html_e( 'Phone Number:', 'your-text-domain' ); ?>
</label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'phone' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'phone' ) ); ?>" type="text"
value="<?php echo esc_attr( $phone ); ?>">
</p>
<?php
}Step 3: Saving and Sanitizing Settings (update())
When the form is submitted, update() decides what actually gets saved. Sanitize every field here, before it reaches the database. This is your one clean checkpoint for incoming data, so don’t skip it.
<?php
/**
* Handles updating settings.
*
* @param array $new_instance New settings.
* @param array $old_instance Previous settings.
* @return array Updated settings.
*/
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = isset( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['address'] = isset( $new_instance['address'] ) ? sanitize_textarea_field( $new_instance['address'] ) : '';
$instance['phone'] = isset( $new_instance['phone'] ) ? sanitize_text_field( $new_instance['phone'] ) : '';
return $instance;
}Step 4: Displaying the Widget on the Front-End (widget())
The widget() method is what your visitors actually see. Wrap your output with the $args markup WordPress hands you, before_widget and before_title and their closing pairs, so the widget inherits the theme’s spacing and styling instead of fighting it. And escape everything on the way out.
<?php
/**
* Outputs the content of the widget.
*
* @param array $args Widget display arguments.
* @param array $instance Saved settings.
*/
public function widget( $args, $instance ) {
// Output the before widget markup.
echo $args['before_widget'];
// Display the widget title if it is set.
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'];
}
// Display the address and phone, with proper escaping.
if ( ! empty( $instance['address'] ) ) {
echo '<p>' . esc_html( $instance['address'] ) . '</p>';
}
if ( ! empty( $instance['phone'] ) ) {
echo '<p>' . esc_html( $instance['phone'] ) . '</p>';
}
// Output the after widget markup.
echo $args['after_widget'];
}Step 5: Registering Your Widget with WordPress (widgets_init Hook)
A widget class does nothing until you register it. Call register_widget() on the widgets_init hook and pass it your class name. Put this in a plugin if you want the widget to survive a theme switch, or in your theme’s functions.php if it belongs to that theme.
<?php
// Function to register the custom widget.
function register_contact_info_widget() {
register_widget( 'Contact_Info_Widget' );
}
add_action( 'widgets_init', 'register_contact_info_widget' );Step 6: Adding Basic CSS Styles
WordPress already wraps each widget in predictable CSS classes, so you can usually style yours straight from the theme stylesheet without touching the PHP. Here’s a small example for the Contact Info widget:
/* Custom styles for Contact Info Widget */
.widget_contact_info_widget {
background-color: #f9f9f9;
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
}
.widget_contact_info_widget p {
margin: 5px 0;
color: #333;
}
That class hooks into the widget_contact_info_widget wrapper WordPress generates for you, so the styles land without any extra markup. Prefer that over hardcoding a <div> into your output.
Crucial Best Practices: Security, Maintainability & More
- Security is not optional:
- Sanitize input in
update()with functions likesanitize_text_field()andsanitize_textarea_field(). - Escape output in
widget()withesc_html(),esc_attr(), andesc_url(). Sanitize going in, escape coming out. - If your widget does anything beyond saving options, add a nonce in
form().
- Sanitize input in
- Follow the standards: stick to the WordPress Coding Standards for PHP, HTML, and CSS. The next person to open this file (probably you) will be grateful.
- Keep it readable: small methods, descriptive names, and a comment wherever the intent isn’t obvious.
- Translate everything: wrap user-facing strings in
__()and_e()with a consistent text domain. - Stay light: keep heavy queries out of
widget(), since it can run on every page load. Cache the output if the work is real.
Bonus Tip: Making Your Widget Translatable
If you want your widget to work in other languages, wrap every visible string in a translation function. The constructor is a good place to see the pattern:
<?php
public function __construct() {
parent::__construct(
'contact_info_widget',
__( 'Contact Info Widget', 'your-text-domain' ),
array( 'description' => __( 'A widget to display contact information.', 'your-text-domain' ) )
);
}Conclusion
That’s the whole shape of a classic widget: extend WP_Widget, build the form, save the settings safely, render the front end. Do it once and the pattern repeats for any widget you’ll ever write.
Two things worth burning into memory. Sanitize on the way in, escape on the way out, every single time. And be honest about whether you need a widget class at all: if the goal is just placing content, a block is less code and less to maintain. Reach for WP_Widget when you’re packaging real, reusable functionality that has to travel with a plugin.


