WP_Widget Class Generator
Generate a WP_Widget subclass with its form, update and widget methods, correctly escaped, plus the registration.
<?php
/**
* The "Recent Items" widget.
*/
defined( 'ABSPATH' ) || exit;
/**
* A classic widget. Since WordPress 5.8 it also appears in the block widget
* screen through the legacy widget block, so it keeps working either way.
*/
class My_Plugin_Recent_Widget extends WP_Widget {
/**
* Registers the widget with its ID base, name and description.
*/
public function __construct() {
parent::__construct(
'my_plugin_recent',
__( 'Recent Items', 'my-plugin' ),
array(
'description' => __( 'A short list of the most recent items.', 'my-plugin' ),
'classname' => 'my_plugin_recent',
)
);
}
/**
* The defaults, used before the widget has been saved once.
*
* @return array
*/
protected function defaults() {
return array(
'title' => 'Recent Items',
'count' => '5',
);
}
/**
* The form in the widgets screen.
*
* @param array $instance Saved settings.
*/
public function form( $instance ) {
$instance = wp_parse_args( (array) $instance, $this->defaults() );
printf(
'<p><label for="%1$s">%2$s</label>',
esc_attr( $this->get_field_id( 'title' ) ),
esc_html__( 'Title', 'my-plugin' )
);
printf(
'<input class="widefat" type="text" id="%1$s" name="%2$s" value="%3$s" /></p>',
esc_attr( $this->get_field_id( 'title' ) ),
esc_attr( $this->get_field_name( 'title' ) ),
esc_attr( $instance['title'] )
);
printf(
'<p><label for="%1$s">%2$s</label>',
esc_attr( $this->get_field_id( 'count' ) ),
esc_html__( 'How many', 'my-plugin' )
);
printf(
'<input class="tiny-text" type="number" step="1" min="0" id="%1$s" name="%2$s" value="%3$s" /></p>',
esc_attr( $this->get_field_id( 'count' ) ),
esc_attr( $this->get_field_name( 'count' ) ),
esc_attr( $instance['count'] )
);
}
/**
* Cleans the submitted settings.
*
* Starting from the defaults means a key you never declared cannot be
* saved, whatever the form posts.
*
* @param array $new_instance Submitted values.
* @param array $old_instance Previously saved values.
* @return array
*/
public function update( $new_instance, $old_instance ) {
$instance = $this->defaults();
$instance['title'] = isset( $new_instance['title'] )
? sanitize_text_field( $new_instance['title'] )
: '';
$instance['count'] = isset( $new_instance['count'] )
? (string) absint( $new_instance['count'] )
: '';
return $instance;
}
/**
* Renders the widget on the front end.
*
* @param array $args Sidebar markup from register_sidebar().
* @param array $instance Saved settings.
*/
public function widget( $args, $instance ) {
$instance = wp_parse_args( (array) $instance, $this->defaults() );
echo wp_kses_post( $args['before_widget'] );
$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
if ( $title ) {
echo wp_kses_post( $args['before_title'] ) . esc_html( $title ) . wp_kses_post( $args['after_title'] );
}
$items = get_posts(
array(
'post_type' => 'post',
'posts_per_page' => absint( $instance['count'] ),
'post_status' => 'publish',
'no_found_rows' => true,
)
);
if ( $items ) {
echo '<ul>';
foreach ( $items as $item ) {
printf(
'<li><a href="%1$s">%2$s</a></li>',
esc_url( get_permalink( $item ) ),
esc_html( get_the_title( $item ) )
);
}
echo '</ul>';
} else {
echo '<p>' . esc_html__( 'Nothing to show yet.', 'my-plugin' ) . '</p>';
}
echo wp_kses_post( $args['after_widget'] );
}
}
/**
* Registers the widget.
*/
function my_plugin_register_widget() {
register_widget( 'My_Plugin_Recent_Widget' );
}
add_action( 'widgets_init', 'my_plugin_register_widget' );
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
Generate a WP_Widget subclass with the three methods it needs, each doing its own job: form() prints the settings, update() cleans them, and widget() renders the front end.
How to use
- Pick the ID base carefully. It is stored with every placed instance, so renaming it later leaves those widgets orphaned in the database.
- Keep a setting called
title. WordPress and most themes expect it, and it gets the theme’sbefore_titlemarkup automatically. - Let
update()start from the defaults. Building the saved array from your own declared keys means a key you never defined cannot be stored. - Escape in
widget(), not inupdate(). Storing escaped values means double escaping the day you output them somewhere else. - Register on
widgets_initwithregister_widget()and the class name.
Example
The part that catches people out is the sidebar markup:
public function widget( $args, $instance ) {
echo wp_kses_post( $args['before_widget'] );
$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
if ( $title ) {
echo wp_kses_post( $args['before_title'] ) . esc_html( $title ) . wp_kses_post( $args['after_title'] );
}
echo wp_kses_post( $args['after_widget'] );
}
$args['before_widget'] and its partner come from register_sidebar(). Skipping them means the widget loses the theme’s wrapper, its ID and its classes.
Pitfalls
- Forgetting
before_widgetandafter_widgetdrops the per-widget ID and classes, which is why “my widget CSS does not apply” is such a common report. update()receives raw input. Sanitizing there and escaping inwidget()is the split; doing both in one place leads to double-escaped output.get_field_id()andget_field_name()exist because WordPress renumbers instances. Hardcoded names break the second copy of the widget.- An unchecked checkbox posts nothing, so
update()has to treat a missing key as off rather than as unchanged. - The ID base is part of the stored option. Changing it after launch leaves the old widgets in the sidebar pointing at a class that no longer claims them.
- Since WordPress 5.8 the widgets screen is block based, and classic widgets appear through the legacy widget block. They still work; they just look different in the admin.
apply_filters( 'widget_title', ... )is what lets other plugins adjust the title. Skipping it quietly breaks those integrations.- A query inside
widget()runs on every page the sidebar appears on. Keep it small and setno_found_rows.
Compatibility
WP_Widget has been the widget base class since WordPress 2.8, and register_widget() on widgets_init since then. The block widgets screen arrived in 5.8 and renders classic widgets through the legacy widget block; show_instance_in_rest controls whether an instance is exposed there. wp_parse_args() and the field helper methods have been stable throughout. The generated code targets PHP 7.0 and up, and the tool runs entirely in your browser.
Frequently asked questions
Should I still write classic widgets?
Why is my widget missing its wrapper?
$args['before_widget'] and $args['after_widget'] were not echoed. They carry the theme’s markup.Why do two copies of the widget fight?
get_field_name(), so both instances post to the same key.Where are widget settings stored?
widget_<id_base>, keyed by instance number. That is why the ID base matters.