SponsoredHosinger Special Offer

WP+ WordPress Plugin Development

Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.

Building a Classic Widget

WordPress Plugin Development Lesson 22 of 27 ~5 min read

Overview

A classic widget is a PHP class that extends WP_Widget, with methods to display the widget, show its settings form in the admin and save those settings — and it can be placed in any widget area the theme provides.

The widget class

<?php
class GDC_Books_Recent_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'gdc_books_recent',                                  // Base ID.
            __( 'Recent Books', 'gdc-books' ),                   // Name in the admin.
            array( 'description' => __( 'Your latest books.', 'gdc-books' ) )
        );
    }

    /**
     * Front-end output.
     */
    public function widget( $args, $instance ) {
        $title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Recent Books', 'gdc-books' );
        $count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;

        $books = get_posts(
            array(
                'post_type'   => 'gdc_book',
                'numberposts' => $count,
            )
        );

        if ( ! $books ) {
            return;
        }

        echo $args['before_widget']; // Markup from the theme's register_sidebar().
        echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title, $instance, $this->id_base ) ) . $args['after_title'];

        echo '<ul>';
        foreach ( $books as $book ) {
            printf( '<li><a href="%s">%s</a></li>', esc_url( get_permalink( $book ) ), esc_html( get_the_title( $book ) ) );
        }
        echo '</ul>';

        echo $args['after_widget'];
    }

    /**
     * Admin settings form.
     */
    public function form( $instance ) {
        $title = $instance['title'] ?? '';
        $count = $instance['count'] ?? 5;
        ?>
        <p>
            <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'gdc-books' ); ?></label>
            <input class="widefat" type="text"
                   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' ) ); ?>"><?php esc_html_e( 'Number of books:', 'gdc-books' ); ?></label>
            <input class="tiny-text" type="number" 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
    }

    /**
     * Sanitises settings before they are saved.
     */
    public function update( $new_instance, $old_instance ) {
        return array(
            'title' => sanitize_text_field( $new_instance['title'] ?? '' ),
            'count' => min( 20, max( 1, absint( $new_instance['count'] ?? 5 ) ) ),
        );
    }
}

add_action( 'widgets_init', function () {
    register_widget( 'GDC_Books_Recent_Widget' );
} );

The four methods

MethodRunsJob
__construct()OnceSets the ID, name and description.
widget()On the front endPrints the widget, wrapped in the theme's before_widget / after_widget.
form()In the adminPrints the settings fields. get_field_id() and get_field_name() keep several copies of the widget from clashing.
update()On saveSanitises and returns the settings to store.

$args['before_widget'] and the title wrappers come from the theme's register_sidebar() call — that is how one widget looks right in every theme. They are trusted markup and are printed as-is.

Widgets today

Since WordPress 5.8 the Widgets screen uses blocks. Classic widgets still work — they appear inside a "Legacy Widget" block — and remain the right tool for plugins supporting classic themes. For anything new and visual, a block (next lesson) is the modern equivalent and works everywhere: posts, pages, widget areas and the Site Editor.

Practice

  • Register the Recent Books widget and add it to a sidebar.
  • Add two copies with different counts and confirm each keeps its own settings.
  • Enter 999 as the count and confirm update() caps it at 20.
Create a free account to save which lessons you've finished. Save my progress