Build a custom WordPress widget with WP Widget

Classic WordPress widgets — the drag-and-drop blocks you add to sidebars and footer areas — have been part of WordPress since version 2.2, and while Gutenberg’s block-based widget editor has largely replaced the old widget admin screen, custom widgets are still the correct tool for building reusable sidebar components in classic themes. A widget is a PHP class that extends WP_Widget and implements four methods: __construct() (registers the widget and sets its title and description), widget() (outputs the front-end HTML), form() (renders the admin settings form), and update() (sanitises and saves the settings). The widget is registered with register_widget() inside the widgets_init action. Theme developers often create widgets for things like recent posts with thumbnails, a custom bio box with a photo, an Instagram feed placeholder, or a social sharing panel — anything that needs dynamic PHP-generated output and an admin-configurable settings form. If you are building a block theme, the equivalent is registering a custom Gutenberg block, but for classic themes and plugin development, WP_Widget is still the right API. Pair your widget with properly enqueued styles and, if needed, with a companion shortcode so editors can also embed the widget’s content inside post content. The example below creates a configurable “Recent Posts with Excerpt” widget.

Problem: You need a custom sidebar widget with an admin settings form that editors can configure without writing code, displaying dynamic content in a classic WordPress theme.

Solution: Add the following code to your functions.php file or a custom plugin:

class Helloadmin_Recent_Posts_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'helloadmin_recent_posts',
            __( 'Recent Posts + Excerpt' ),
            [ 'description' => __( 'Shows recent posts with a configurable excerpt.' ) ]
        );
    }

    /** Front-end output */
    public function widget( $args, $instance ) {
        $title  = apply_filters( 'widget_title', $instance['title'] ?? '' );
        $number = absint( $instance['number'] ?? 5 );

        $posts = get_posts( [
            'numberposts' => $number,
            'post_status' => 'publish',
        ] );

        echo $args['before_widget'];
        if ( $title ) {
            echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
        }
        echo '<ul class="helloadmin-recent-posts">';
        foreach ( $posts as $post ) {
            printf(
                '<li><a href="%s">%s</a><p>%s</p></li>',
                esc_url( get_permalink( $post ) ),
                esc_html( $post->post_title ),
                esc_html( wp_trim_words( $post->post_content, 20 ) )
            );
        }
        echo '</ul>';
        echo $args['after_widget'];
    }

    /** Admin form */
    public function form( $instance ) {
        $title  = $instance['title']  ?? 'Recent Posts';
        $number = $instance['number'] ?? 5;
        printf(
            '<p><label for="%s">Title:</label>
             <input class="widefat" id="%s" name="%s" type="text" value="%s"></p>
             <p><label for="%s">Number of posts:</label>
             <input id="%s" name="%s" type="number" value="%d" min="1" max="20" style="width:50px"></p>',
            $this->get_field_id('title'),   $this->get_field_id('title'),
            $this->get_field_name('title'),  esc_attr($title),
            $this->get_field_id('number'),  $this->get_field_id('number'),
            $this->get_field_name('number'), absint($number)
        );
    }

    /** Save settings */
    public function update( $new_instance, $old_instance ) {
        return [
            'title'  => sanitize_text_field( $new_instance['title'] ),
            'number' => absint( $new_instance['number'] ),
        ];
    }
}

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

NOTE: WordPress 5.8 introduced the block-based widget editor which replaces the classic Widgets admin screen. Classic widgets still work, but they appear in the “Legacy” section in the new editor. If your widget does not appear after registration, check that the active theme calls register_sidebar() to define at least one widget area — a theme with no registered sidebars will not show the Widgets menu item at all.