Skip to main content
Blog|
How-to guides

How to create a custom WordPress widget

|
May 20, 2026|10 min read
HOW-TO GUIDESHow to create a customWordPress widgetHOSTNEYhostney.comMay 20, 2026

Short answer: to build a classic custom widget, create a PHP class that extends WP_Widget , define its four methods ( __construct , widget , form , update ), and register it with register_widget() on the widgets_init hook. Drop the code into your theme’s functions.php or a small plugin, and the widget appears under Appearance > Widgets ready to place in any widget area.

This guide walks through that classic approach in full, then covers the block-based widget editor introduced in WordPress 5.8, and explains which one you should actually use depending on your theme.

Classic widget or block widget: which applies to you#

Since WordPress 5.8, the Appearance > Widgets screen uses the block editor. You add blocks to widget areas the same way you add blocks to a post. But the underlying WP_Widget API never went away – classic widgets still register, still render, and still show up in the block-based screen as a “Legacy Widget” block.

So there are two different things people mean by “custom widget”:

  • A reusable PHP widget built on the WP_Widget class. This is what you want when the widget needs settings, dynamic data, or has to be reusable across sites. It works in both classic and block widget screens.
  • A custom Gutenberg block placed in a widget area. This is just a block, registered with the Block API, and it happens to live in a sidebar.

If you are a developer building something configurable and portable, the WP_Widget class is still the right tool, and that is what most of this article covers. If your theme is a block theme (it has no Appearance > Widgets screen at all, only the Site Editor), classic widgets do not have a home and you should build a block instead.

Building a classic widget with the WP_Widget class#

A classic widget is a single PHP class. The structure is always the same: extend WP_Widget , then implement four methods. Here is a complete, working widget that displays a configurable heading and a short message.

class Hostney_Notice_Widget extends WP_Widget {

    // 1. Register the widget with WordPress
    public function __construct() {
        parent::__construct(
            'hostney_notice_widget',                // Base ID (unique)
            'Notice box',                           // Name shown in admin
            array( 'description' => 'A simple notice box with a heading and message.' )
        );
    }

    // 2. Front-end output
    public function widget( $args, $instance ) {
        $title   = ! empty( $instance['title'] ) ? $instance['title'] : '';
        $message = ! empty( $instance['message'] ) ? $instance['message'] : '';

        echo $args['before_widget'];

        if ( $title ) {
            echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
        }

        echo '<p>' . esc_html( $message ) . '</p>';

        echo $args['after_widget'];
    }

    // 3. Admin form
    public function form( $instance ) {
        $title   = ! empty( $instance['title'] ) ? $instance['title'] : '';
        $message = ! empty( $instance['message'] ) ? $instance['message'] : '';
        ?>
        <p>
            <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">Title:</label>
            <input class="widefat"
                   id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
                   name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
                   type="text"
                   value="<?php echo esc_attr( $title ); ?>">
        </p>
        <p>
            <label for="<?php echo esc_attr( $this->get_field_id( 'message' ) ); ?>">Message:</label>
            <textarea class="widefat"
                      id="<?php echo esc_attr( $this->get_field_id( 'message' ) ); ?>"
                      name="<?php echo esc_attr( $this->get_field_name( 'message' ) ); ?>"
                      rows="4"><?php echo esc_textarea( $message ); ?></textarea>
        </p>
        <?php
    }

    // 4. Save form input
    public function update( $new_instance, $old_instance ) {
        $instance            = array();
        $instance['title']   = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
        $instance['message'] = ! empty( $new_instance['message'] ) ? sanitize_textarea_field( $new_instance['message'] ) : '';
        return $instance;
    }
}

Each method has one job:

  • __construct() gives the widget a base ID, the name shown in the admin, and an options array (the description is what users see under the widget title in the picker).
  • widget() is the only method visitors ever trigger. It receives $args (theme-supplied wrapper markup) and $instance (the saved settings) and prints the front-end HTML.
  • form() draws the settings form inside the widget admin. Always use get_field_id() and get_field_name() so multiple copies of the same widget do not collide.
  • update() runs when settings are saved. It is your one chance to sanitize input before it reaches the database.

Registering the widget#

A widget class does nothing until WordPress is told about it. Register it on the widgets_init action:

function hostney_register_widgets() {
    register_widget( 'Hostney_Notice_Widget' );
}
add_action( 'widgets_init', 'hostney_register_widgets' );

You can register as many widgets as you like inside that one callback. After this runs, “Notice box” appears in Appearance > Widgets (or as a Legacy Widget block) and can be dropped into any registered widget area.

Where to put the code#

You have two choices, and the right one depends on whether the widget belongs to the design or the site.

Put it in functions.php if the widget is part of a specific theme and only makes sense with that design. If you go this route, use a child theme so a theme update does not erase your work – the same rule applies when you create a custom WordPress theme from scratch.

Put it in a small plugin if the widget should survive a theme switch – for example, a “contact details” widget you want on every site you build. A plugin is just a PHP file with a header comment in wp-content/plugins/ :

<?php
/**
 * Plugin Name: Hostney Custom Widgets
 * Description: Registers custom widgets for this site.
 * Version: 1.0.0
 */

// ... widget class and register_widget() call go here ...

Activate it from the Plugins screen and the widget is available regardless of which theme is active. If you are new to that screen, our guide on how to install plugins in WordPress covers the basics.

Adding a custom widget area (sidebar)#

If your theme has no suitable place for the widget, register your own widget area with register_sidebar() , again on widgets_init :

function hostney_register_sidebar() {
    register_sidebar( array(
        'name'          => 'Custom footer column',
        'id'            => 'custom-footer-column',
        'description'   => 'Appears in the footer.',
        'before_widget' => '<div class="footer-widget %2$s">',
        'after_widget'  => '</div>',
        'before_title'  => '<h3 class="footer-widget-title">',
        'after_title'   => '</h3>',
    ) );
}
add_action( 'widgets_init', 'hostney_register_sidebar' );

The before_widget and after_widget strings are exactly the $args your widget’s widget() method receives. To actually display the area, call dynamic_sidebar() in the matching theme template:

<?php if ( is_active_sidebar( 'custom-footer-column' ) ) : ?>
    <div class="footer-column">
        <?php dynamic_sidebar( 'custom-footer-column' ); ?>
    </div>
<?php endif; ?>

The is_active_sidebar() check keeps the wrapper markup from rendering when the area is empty.

Block-based widgets in WordPress 5.8 and later#

The block widget editor does not replace the WP_Widget API – it changes the screen you place widgets on. A classic widget still works; it just shows up wrapped in a Legacy Widget block.

If you specifically want a block instead of a classic widget, you are no longer building a widget at all – you are registering a Gutenberg block and placing it in a widget area. The minimum is a block.json file plus a registration call:

function hostney_register_blocks() {
    register_block_type( __DIR__ . '/blocks/notice' );
}
add_action( 'init', 'hostney_register_blocks' );

The block’s block.json describes its attributes, its editor script, and its render behavior. A block can be dynamic (rendered by PHP through a render_callback ) or static (saved HTML). Once registered, it appears in the block inserter on the Widgets screen the same as in the post editor.

The practical rule: if you need a settings form and reuse across sites, build a WP_Widget class. If you want an inline editing experience that matches the rest of the modern editor, build a block. Block themes – those with no Appearance > Widgets screen at all – leave you no choice but the block route, since classic widget areas only exist in classic and hybrid themes.

Keeping classic widgets on a modern install#

Some sites are not ready for the block widget screen – a client is used to the old interface, or a third-party widget renders badly inside it. Installing the official Classic Widgets plugin restores the pre-5.8 widget screen. It is maintained by the WordPress core team and is a supported, long-term-safe choice for sites that need it. It changes only the admin screen; the widgets themselves are unaffected.

Common mistakes to avoid#

A few errors come up again and again when developers write their first widget:

  • Forgetting to escape output. Everything printed in widget() must be escaped – esc_html() , esc_attr() , esc_url() . A widget that echoes raw saved input is a stored XSS hole.
  • Skipping sanitization in update() . update() is the gate between the form and the database. Run sanitize_text_field() , sanitize_textarea_field() , or a type-specific check on every field.
  • Hardcoding field IDs. Use get_field_id() and get_field_name() . Hardcoded IDs break the moment a user adds a second copy of the widget.
  • Registering on the wrong hook. register_widget() belongs on widgets_init , not init . register_block_type() for a block belongs on init .
  • Editing the parent theme. Adding widget code to a parent theme’s functions.php means the next theme update wipes it. Use a child theme or a plugin.
  • A non-unique base ID. The first argument to parent::__construct() must be unique across the whole site, or it collides with another widget.

When something does not render, turn on WP_DEBUG so PHP notices surface instead of failing silently – our guide to wp-config.php settings explains how.

Styling your widget#

Widget output is just HTML, so it is styled like anything else. Hook a stylesheet with wp_enqueue_scripts rather than inlining <style> tags:

function hostney_widget_styles() {
    wp_enqueue_style(
        'hostney-widgets',
        get_stylesheet_directory_uri() . '/css/widgets.css',
        array(),
        '1.0.0'
    );
}
add_action( 'wp_enqueue_scripts', 'hostney_widget_styles' );

For small tweaks you may not need a separate file at all – the Customizer’s Additional CSS panel is enough, as covered in how to edit and add custom CSS in WordPress. If your widget also needs to look right inside the block editor preview, the same logic applies as for adding custom styles to the WordPress visual editor.

Testing your widget#

Before calling a widget finished, run through this checklist:

  1. The widget appears in Appearance > Widgets (or the block inserter for a Legacy Widget).
  2. Adding it to a widget area and saving works without an error.
  3. The settings form shows saved values correctly after a page reload.
  4. Two copies of the widget on the same page keep separate settings.
  5. The front end renders the saved values and escapes them.
  6. With WP_DEBUG on, no notices or warnings appear.

If the widget area itself is a custom one, also confirm dynamic_sidebar() is called in the right template and that is_active_sidebar() hides the wrapper when empty. Widget areas are closely related to other Appearance tools – if you are building out a theme’s structure, the same template logic applies when you add and manage menus in WordPress.

How Hostney handles custom widget code#

Custom widgets are code, and code needs somewhere to live and a safe way to test. On Hostney every site runs in its own isolated container, so a widget that throws a fatal error cannot take down anything but the site you are working on. You get full file access through SSH/SFTP, FTPS, or the built-in file manager to drop a child theme or a custom plugin into place – whichever you choose for your widget.

Hostney also makes it straightforward to flip WP_DEBUG on while you develop and back off for production, and a daily-updated PHP environment means the WP_Widget API and the block APIs behave exactly as the current WordPress documentation describes. When your widget is ready, the same workflow ships it: edit the file, reload the page, see the result.

Summary#

A custom WordPress widget is, at its core, a PHP class that extends WP_Widget with four methods – __construct , widget , form , and update – registered on the widgets_init hook with register_widget() . That classic approach still works everywhere, including the block widget screen, and remains the right choice for anything configurable and reusable. Build a Gutenberg block instead when you want inline editing or when a block theme leaves you no widget screen at all. Whichever route you take, keep the code in a child theme or plugin, escape every line of output, sanitize every saved field, and test with WP_DEBUG on before you ship.

Related articles