2026年11月13日
2026年11月13日
Create a Custom Widget in a WordPress Plugin (WP_Widget, WP-CLI)
This article is a localized edition of the Japanese original. Read the Japanese original
WordPress widgets are components you place in "widget areas" such as sidebars and footers. To add your own widget from a plugin, create a class that extends WP_Widget and pass it to register_widget() on the widgets_init action.
The code in this article was tested in November 2026 on WordPress 7.1 and PHP 8.3, from activation through adding and displaying the widget.
Where widgets can be used
Custom widgets work with themes that register widget areas (sidebars).
- Classic themes (such as Twenty Twenty-One): add widgets from "Appearance" → "Widgets". Since WordPress 5.8 this screen is edited with blocks (the block widget editor), but widgets built with
WP_Widgetcan still be added as before. - Block themes (such as Twenty Twenty-Five): there are no widget areas, so "Widgets" does not appear under "Appearance". If you want a component for the Site Editor, build it as a block rather than a widget.
Build the widget plugin
Create a wp-content/plugins/my-recent-posts-widget/ folder and save the following inside it as my-recent-posts-widget.php. This widget shows a set number of recent posts.
<?php
/**
* Plugin Name: My Recent Posts Widget
* Description: Sample widget that shows recent posts
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Myplugin_Recent_Posts_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'myplugin_recent_posts', // Base name for the ID (id_base)
'MY: Recent posts', // Name shown in the admin screen
array(
'description' => 'Shows a list of recent posts',
'classname' => 'myplugin-recent-posts',
'show_instance_in_rest' => true,
)
);
}
// Output on the site
public function widget( $args, $instance ) {
$title = apply_filters( 'widget_title', $instance['title'] ?? '', $instance, $this->id_base );
$count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
$posts = get_posts(
array(
'numberposts' => $count,
'post_status' => 'publish',
)
);
if ( ! $posts ) {
return;
}
echo $args['before_widget'];
if ( $title ) {
echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
}
echo '<ul class="myplugin-recent-list">';
foreach ( $posts as $post ) {
printf(
'<li><a href="%s">%s</a></li>',
esc_url( get_permalink( $post ) ),
esc_html( get_the_title( $post ) )
);
}
echo '</ul>';
echo $args['after_widget'];
}
// Settings form in the admin screen
public function form( $instance ) {
$title = $instance['title'] ?? 'Recent posts';
$count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">Title:</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( $title ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">Number of posts:</label>
<input type="number" class="tiny-text" min="1" max="20"
id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
value="<?php echo esc_attr( $count ); ?>">
</p>
<?php
}
// Clean up the values before saving
public function update( $new_instance, $old_instance ) {
$count = absint( $new_instance['count'] ?? 5 );
return array(
'title' => sanitize_text_field( $new_instance['title'] ?? '' ),
'count' => min( max( $count, 1 ), 20 ),
);
}
}
add_action(
'widgets_init',
function () {
register_widget( 'Myplugin_Recent_Posts_Widget' );
}
);
After saving, activate "My Recent Posts Widget" under "Plugins" → "Installed Plugins". With a classic theme, go to "Appearance" → "Widgets", add a "Legacy Widget" block to a widget area, and choose "MY: Recent posts" from the list of widgets.
What the four methods do
| Method | Role |
|---|---|
__construct() |
Passes the base name for the ID (id_base), display name, description and so on to parent::__construct() |
widget( $args, $instance ) |
The HTML output on the site. $args holds the wrapper tags defined by the widget area, and $instance holds the saved settings |
form( $instance ) |
The settings form shown in the admin screen. Build each field's id and name with get_field_id() and get_field_name() |
update( $new_instance, $old_instance ) |
Cleans up the input before saving and returns it. The returned array is saved as is |
Do not add type declarations to the parameters
The widget(), form() and update() methods of WP_Widget have no parameter types. If you add types in the child class, as in widget( array $args, array $instance ), PHP 8 treats the method as incompatible with the parent, and loading the plugin causes a fatal error like this:
PHP Fatal error: Declaration of Bad_Widget::widget(array $args, array $instance): void must be compatible with WP_Widget::widget($args, $instance)
Write the child class parameters without types, just like the parent class.
Escaping the output
In widget(), the title is output with esc_html() and URLs with esc_url(). Values such as $args['before_widget'] are HTML provided by the theme (the code that registers the widget area), so they are output as is.
Setting show_instance_in_rest to true exposes the widget's settings as-is in the REST API (which the block widget editor uses). WordPress's built-in widgets also set it to true. Do not set it to true for widgets whose settings hold values you do not want exposed, such as passwords.
Add a widget area
If your theme has no widget area, or you want an extra area for your plugin, register one with register_sidebar(). Registering one in a block theme also makes "Appearance" → "Widgets" appear, but because the templates have no way to output widget areas, nothing is shown on the site.
<?php
add_action( 'widgets_init', function () {
register_sidebar(
array(
'id' => 'myplugin-sidebar',
'name' => 'Plugin sidebar',
'description' => 'Widget area added by the plugin',
'before_widget' => '<section id="%1$s" class="widget %2$s">',
'after_widget' => '</section>',
'before_title' => '<h2 class="widget-title">',
'after_title' => '</h2>',
)
);
} );
In a classic theme template file (such as sidebar.php), output it only when widgets have been placed there.
<?php if ( is_active_sidebar( 'myplugin-sidebar' ) ) : ?>
<aside class="myplugin-sidebar">
<?php dynamic_sidebar( 'myplugin-sidebar' ); ?>
</aside>
<?php endif; ?>
In before_widget, %1$s is replaced with the widget ID (for example myplugin_recent_posts-2) and %2$s with the class name set in classname.
Add and check widgets with WP-CLI
The WP-CLI wp widget command adds, changes and deletes widgets in widget areas. Specify the widget type by its id_base (myplugin_recent_posts in this example) and pass settings as --field=value.
# List widget areas
wp sidebar list --fields=name,id
# Add at position 1
wp widget add myplugin_recent_posts myplugin-sidebar 1 --title="New posts" --count=3
# Check the placed widgets and their settings
wp widget list myplugin-sidebar --fields=name,id,position,options
# Change settings (fields you omit revert to update() defaults, so pass them all)
wp widget update myplugin_recent_posts-1 --title="Latest posts" --count=3
# Delete
wp widget delete myplugin_recent_posts-1
In our test, passing only --title to wp widget update reset count to 5, the default in update(). When making changes, pass every field you want to keep.
Going back to the old widgets screen
To stop using the block widget editor and return to the widgets screen used up to WordPress 5.7, return false from the use_widgets_block_editor filter.
<?php
add_filter( 'use_widgets_block_editor', '__return_false' );
Activating the official "Classic Widgets" plugin has the same result.
Related articles
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)