Building a custom widget using the WP_Widget class
WordPress widgets let you drop reusable blocks of content into any sidebar or footer area. While the default set covers most common cases, sometimes you need a widget that does exactly one specific thing. Building a custom widget with the WP_Widget class is straightforward.
Problem: How do you create a reusable custom widget in WordPress that can be placed in any registered sidebar?
Solution: The example below creates a simple "Latest Posts by Category" widget:
class My_Posts_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'my_posts_widget',
__( 'Posts by Category', 'textdomain' ),
[ 'description' => __( 'Displays latest posts from a chosen category.', 'textdomain' ) ]
);
}
// Front-end output
public function widget( $args, $instance ) {
$cat_id = ! empty( $instance['cat_id'] ) ? absint( $instance['cat_id'] ) : 0;
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
echo $args['before_widget'];
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'];
}
$posts = new WP_Query( [
'cat' => $cat_id,
'posts_per_page' => $count,
'no_found_rows' => true,
] );
if ( $posts->have_posts() ) {
echo '<ul>';
while ( $posts->have_posts() ) {
$posts->the_post();
echo '<li><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a></li>';
}
echo '</ul>';
wp_reset_postdata();
}
echo $args['after_widget'];
}
// Back-end form
public function form( $instance ) {
$title = $instance['title'] ?? '';
$cat_id = $instance['cat_id'] ?? '';
$count = $instance['count'] ?? 5;
?>
<p>
<label><?php esc_html_e( 'Title:', 'textdomain' ); ?>
<input class="widefat" name="<?php echo $this->get_field_name( 'title' ); ?>"
value="<?php echo esc_attr( $title ); ?>">
</label>
</p>
<p>
<label><?php esc_html_e( 'Category ID:', 'textdomain' ); ?>
<input class="widefat" name="<?php echo $this->get_field_name( 'cat_id' ); ?>"
value="<?php echo esc_attr( $cat_id ); ?>">
</label>
</p>
<p>
<label><?php esc_html_e( 'Number of posts:', 'textdomain' ); ?>
<input class="tiny-text" name="<?php echo $this->get_field_name( 'count' ); ?>"
value="<?php echo absint( $count ); ?>" type="number" min="1" max="20">
</label>
</p>
<?php
}
// Save widget settings
public function update( $new, $old ) {
return [
'title' => sanitize_text_field( $new['title'] ),
'cat_id' => absint( $new['cat_id'] ),
'count' => absint( $new['count'] ),
];
}
}
add_action( 'widgets_init', function() {
register_widget( 'My_Posts_Widget' );
} );
NOTE: Always sanitize input in update() and escape output in widget(). Call wp_reset_postdata() after a custom WP_Query inside a widget to restore the global post object.