October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
VGSources
Blog

How to Develop Custom Widgets in WordPress: A Complete PHP and Block-Editor Guide

Build a production-ready WordPress custom widget: extend WP_Widget, render with theme wrappers, add sanitized settings, register on widgets_init, and understand block-based widget editing.
WorldWordPress: A Complete PHP and Block-Editor Guide Length14 min Posted Quest giverVGSources Team

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To develop a classic custom WordPress widget, create a PHP class that extends WP_Widget, output the front-end markup from widget(), add administration controls with form(), sanitize saved values in update(), and register the class on widgets_init with register_widget(). The active theme must also provide at least one widget area (often called a sidebar) before an administrator can place the widget.

This guide builds a working widget from the ground up, explains how each method fits into WordPress’s lifecycle, and shows what changes when a site uses block-based widget editing.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What a WordPress widget actually is

A widget is a reusable piece of content or functionality that a site owner places into a widget area. A widget area is the container supplied by the theme; it may be a traditional sidebar, a footer column, a header region, or another location registered by the theme. The widget and the area are separate: your PHP class defines what the widget does, while the theme decides which areas exist and how those areas are wrapped in markup.

The WordPress Theme Handbook summarizes the relationship clearly: “A widget adds content and features to a widget area (also called a sidebar).” A correctly coded widget can therefore be moved between every compatible area without hard-coding a particular sidebar name or page template.

Before you write code

Choose a safe place for the code

Put a production widget in a small plugin rather than in a theme’s functions.php. A plugin keeps the widget available if the site changes themes. For a one-theme project, theme code can work, but the widget will disappear from the Widgets screen when that theme is deactivated unless the class is also loaded elsewhere.

Create a plugin file such as vgs-custom-widgets.php in a directory under wp-content/plugins. Use a unique PHP namespace or class prefix so your class and its base ID cannot collide with another plugin.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Confirm that the theme has a widget area

Your widget will not appear in a usable location unless the active theme registers at least one sidebar or widget area. In a classic theme, this is commonly done with register_sidebar(). The area’s title, ID, and wrapper strings belong to the theme, not to the widget class. If no area is registered, first enable a theme that exposes one or add an area to the theme’s setup code.

Decide whether the site uses classic or block-based widget editing

WordPress sites can expose a classic Widgets screen, block-based widget editing, or a mixture during a transition. Classic PHP widgets continue to use the WP_Widget API. Core also includes WP_Widget_Block, introduced in WordPress 5.8.0, which allows block content to participate in widget areas. Check the active site’s Appearance menu before choosing an authoring experience.

Build the smallest useful custom widget

1. Define a class that extends WP_Widget

The class name is your PHP implementation; the first argument passed to the parent constructor is the stable base ID WordPress uses internally. Keep that ID short, unique, and unchanged after release. Changing it later can make existing settings look like a new widget and leave old instances orphaned.

<?php
namespace VGSCustomWidgets;

class GameTipWidget extends WP_Widget
{
    public function __construct()
    {
        parent::__construct(
            'vgs_game_tip',
            'Game Tip',
            array(
                'description' => 'Displays a short gaming tip in a widget area.',
            )
        );
    }

    public function widget($args, $instance)
    {
        $title = isset($instance['title']) ? $instance['title'] : '';
        $tip   = isset($instance['tip']) ? $instance['tip'] : '';

        echo $args['before_widget'];

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

        echo '<p>' . esc_html($tip) . '</p>';
        echo $args['after_widget'];
    }
}

The class extends the global WP_Widget because the example uses a namespace. If your class is not namespaced, write extends WP_Widget instead. The constructor calls parent::__construct() with a stable base ID, a human-readable name, and optional metadata such as a description.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. Implement widget() for front-end output

WordPress calls widget($args, $instance) when it renders an instance. $args contains the display arguments supplied by the selected widget area, including the theme’s wrapper markup. $instance contains the saved settings for that particular instance.

Use the four wrapper values supplied by the theme: before_widget, before_title, after_title, and after_widget. Printing those values lets the widget inherit the active theme’s classes, accessibility attributes, spacing, and layout conventions. Do not replace them with a hard-coded outer div unless you have a specific reason to add another inner wrapper.

Only print a title when one exists. Escaping depends on context: use esc_html() for plain text, esc_attr() for an HTML attribute, esc_url() for a URL, and an explicitly permitted HTML sanitizer when the setting is intended to contain markup. Escaping at output time protects against a value that entered the database through an unexpected path or was changed by another extension.

Add administrator settings with form()

A widget with fixed output can omit a settings form. Most practical widgets need per-instance controls, however: a title, a selected category, a number of items, or a display preference. WordPress calls form($instance) to render those controls in the Widgets screen or in a block-based widget editor’s legacy-widget interface.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
public function form($instance)
{
    $title = isset($instance['title']) ? $instance['title'] : '';
    $tip   = isset($instance['tip']) ? $instance['tip'] : '';
    ?>
    <p>
        <label for="<?php echo esc_attr($this->get_field_id('title')); ?>">
            <?php esc_html_e('Title:', 'vgs-custom-widgets'); ?>
        </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('tip')); ?>">
            <?php esc_html_e('Tip:', 'vgs-custom-widgets'); ?>
        </label>
        <textarea
            class="widefat"
            id="<?php echo esc_attr($this->get_field_id('tip')); ?>"
            name="<?php echo esc_attr($this->get_field_name('tip')); ?>"
            rows="4"
        ><?php echo esc_textarea($tip); ?></textarea>
    </p>
    <?php
}

Use get_field_id() and get_field_name() rather than inventing input names. WordPress adds the correct instance-specific identifiers, which matters when a page contains several copies of the same widget. Labels should point to the generated field ID, and values must be escaped for the attribute or textarea context.

Validate and sanitize settings in update()

When an administrator saves an instance, WordPress passes the submitted values as $new_instance and the previous values as $old_instance. Return the sanitized array you want stored. Treat this method as the boundary between untrusted form data and persistent settings.

public function update($new_instance, $old_instance)
{
    $instance = array();

    $instance['title'] = isset($new_instance['title'])
        ? sanitize_text_field($new_instance['title'])
        : '';

    $instance['tip'] = isset($new_instance['tip'])
        ? sanitize_textarea_field($new_instance['tip'])
        : '';

    return $instance;
}

Choose a sanitizer that matches the setting. sanitize_text_field() is appropriate for a single-line label; sanitize_textarea_field() preserves intended line breaks while removing unsafe content from a plain-text textarea. For a URL, use URL sanitization and escape it with esc_url() when printing. For a number, convert and validate the range instead of accepting arbitrary text. If a field is deliberately rich text, use a narrowly defined HTML allowlist and still escape or sanitize at output according to its context.

Sanitizing in update() does not replace output escaping. Values can be changed by imports, database edits, or other code after saving, so escape every value when it is rendered.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Register the widget at the correct time

WordPress runs its widget initialization routine, registers default widgets, and fires the widgets_init action. Attach your registration callback to that action and call register_widget() from the callback.

add_action('widgets_init', function () {
    register_widget('VGS\CustomWidgets\GameTipWidget');
});

In a namespaced file, the string passed to register_widget() must contain the complete class name, including its namespace. You can also pass a WP_Widget instance, but registering the subclass name is usually simpler because WordPress can instantiate it when needed.

If you prefer a named callback, the equivalent pattern is:

function vgs_register_custom_widgets()
{
    register_widget('VGS\CustomWidgets\GameTipWidget');
}
add_action('widgets_init', 'vgs_register_custom_widgets');

Load the class before the callback runs. A missing class produces a registration failure, while registering on an earlier hook can run before WordPress has prepared its widget system. The stable base ID in the constructor must also be unique across all installed plugins and themes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Complete plugin example

The following combines the class and registration into one minimal plugin file. The header makes the file recognizable in the Plugins screen; the class supplies a configurable title and tip; and the action registers it at the expected lifecycle point.

<?php
/**
 * Plugin Name: VGS Custom Widgets
 * Description: Adds a configurable game-tip widget.
 * Version: 1.0.0
 */

namespace VGSCustomWidgets;

if (!defined('ABSPATH')) {
    exit;
}

class GameTipWidget extends WP_Widget
{
    public function __construct()
    {
        parent::__construct(
            'vgs_game_tip',
            'Game Tip',
            array(
                'description' => 'Displays a short gaming tip in a widget area.',
            )
        );
    }

    public function widget($args, $instance)
    {
        $title = isset($instance['title']) ? $instance['title'] : '';
        $tip   = isset($instance['tip']) ? $instance['tip'] : '';

        echo $args['before_widget'];

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

        if ($tip !== '') {
            echo '<p>' . esc_html($tip) . '</p>';
        }

        echo $args['after_widget'];
    }

    public function form($instance)
    {
        $title = isset($instance['title']) ? $instance['title'] : '';
        $tip   = isset($instance['tip']) ? $instance['tip'] : '';
        ?>
        <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('tip')); ?>">Tip:</label>
            <textarea
                class="widefat"
                id="<?php echo esc_attr($this->get_field_id('tip')); ?>"
                name="<?php echo esc_attr($this->get_field_name('tip')); ?>"
                rows="4"
            ><?php echo esc_textarea($tip); ?></textarea>
        </p>
        <?php
    }

    public function update($new_instance, $old_instance)
    {
        return array(
            'title' => isset($new_instance['title'])
                ? sanitize_text_field($new_instance['title'])
                : '',
            'tip' => isset($new_instance['tip'])
                ? sanitize_textarea_field($new_instance['tip'])
                : '',
        );
    }
}

add_action('widgets_init', function () {
    register_widget('VGS\CustomWidgets\GameTipWidget');
});

Activate the plugin, open the site’s widget-management screen, add “Game Tip” to an available area, enter a title and tip, and save. The widget’s front-end output should appear inside the theme-provided wrappers.

How to place the widget in the WordPress admin

Classic Widgets screen

  1. Activate the plugin containing the class.
  2. Open the WordPress dashboard and go to Appearance > Widgets.
  3. Choose an available widget area such as a sidebar or footer column.
  4. Add the widget named by the second argument to parent::__construct().
  5. Fill in the fields generated by form(), then save.
  6. Visit a front-end page that displays that area and verify the wrappers, title, and content.

Block-based widget editing

On a site using block-based widget editing, the Widgets screen presents areas as block-editor canvases. A classic WP_Widget can still be offered through the editor’s legacy-widget block, while native blocks are authored directly in the canvas. The exact controls and visual arrangement depend on the WordPress version, active theme, and enabled plugins.

WP_Widget_Block is a core class introduced in WordPress 5.8.0 for representing block content in widget areas. It does not turn an arbitrary PHP widget into a native block; your class still follows the WP_Widget contract. If you need a true block experience, develop and register a block separately, then use it in the widget area.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Classic PHP widgets versus block-based widgets

Aspect Classic PHP widget Block-based widget editing
Authoring model A PHP class extending WP_Widget, with optional form() and update() methods. Block content assembled in a block-editor canvas; classic widgets may appear through a legacy-widget interface.
Registration Attach a callback to widgets_init and call register_widget(). Use the block registration path for the block implementation; core also provides WP_Widget_Block.
Settings UI Fields generated by the widget’s form() method. Block controls supplied by the block editor, with classic widget forms available where legacy support is exposed.
Rendering path widget($args, $instance) prints markup using the theme’s four wrapper arguments. The block editor stores and renders block content; a classic widget still renders through its PHP method.
Version note Uses the established WP_Widget API. WP_Widget_Block was introduced in WordPress 5.8.0.

There is no universal feature matrix for every third-party block or theme. Test the editor mode actually enabled on the target site, especially if a theme changes between classic and block-based widget areas.

Theme wrappers and markup compatibility

Never assume every theme uses the same heading level, CSS class, or outer element. The theme supplies the wrapper strings in $args. A title might be wrapped in an h2 in one theme and an h3 in another. Printing the values unchanged preserves that contract.

Add your own inner elements only where they represent your widget’s content. Keep class names scoped to your plugin to avoid accidentally inheriting unrelated theme styles. If your widget emits links, buttons, images, or data attributes, escape each value for its destination context rather than escaping the whole HTML string after concatenation.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Security, validation, and accessibility checklist

  • Use a unique, stable widget base ID and a distinctive class or namespace.
  • Sanitize every submitted setting in update() before returning the stored array.
  • Escape plain text with esc_html(), attributes with esc_attr(), URLs with esc_url(), and textarea values with esc_textarea().
  • Allow HTML only when the feature requires it, and restrict that HTML to an intentional allowlist.
  • Use the theme’s before_widget, before_title, after_title, and after_widget values.
  • Use a real label for each form control and connect it to the generated field ID.
  • Provide sensible defaults when an instance has never been saved or when an older instance lacks a newly added setting.
  • Keep front-end output valid when a title or content field is empty.

Troubleshooting common failures

The widget does not appear in the Widgets screen

Confirm that the plugin is active, the PHP file loads without a syntax error, and the registration callback is attached to widgets_init. If the class is namespaced, verify that the fully qualified class name passed to register_widget() is correct. A duplicate base ID can also cause confusing behavior when two implementations claim the same identifier.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The widget appears, but there is nowhere to place it

The active theme may not expose a widget area. Check the theme’s widget-area configuration or add a registered sidebar in the theme code. A widget class cannot create a placement area by itself.

Settings disappear after saving

Make sure each input uses get_field_name() and that update() returns an array containing the same keys that widget() and form() read. A mismatch such as saving description but reading tip makes the value appear to vanish. Also check that sanitization is not rejecting the value unintentionally.

Several instances overwrite one another

Hard-coded input names are the usual cause. Generate both IDs and names with the widget methods so WordPress can distinguish each instance. Do not use one global option for values that should differ between placements.

The layout looks wrong in one theme

Inspect the actual wrapper values passed in $args and confirm that your output includes all four theme wrappers. Then check for CSS selectors that assume a particular heading level or class. Themes are free to define different markup, so test every area where the widget will be used.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Markup or scripts appear in a text setting

Sanitize the value in update() and escape it in widget(). If the field is supposed to accept rich text, define the allowed HTML deliberately instead of printing the raw database value.

Testing a widget before release

  1. Activate the plugin on a development site running the WordPress version you support.
  2. Confirm the widget is listed after the widgets_init action has run.
  3. Add one instance to each relevant widget area and save it.
  4. Add a second instance with different values to verify that IDs and settings remain independent.
  5. Test empty, very long, and unusual text values, including characters such as quotes and angle brackets.
  6. Inspect the generated HTML to ensure the theme wrappers are present and properly nested.
  7. Switch between available themes or widget areas to catch assumptions about wrappers and heading levels.
  8. If the site uses block-based widget editing, test the legacy-widget path and any native block alternative separately.
  9. Deactivate the plugin and reactivate it to confirm that registration is deterministic and saved instances return when the class is available again.

Extending the pattern safely

Once the basic widget works, add one setting at a time and keep the data shape backwards-compatible. Read new keys with defaults so existing instances continue to render. For a selectable list, validate that the submitted value is one of the permitted choices rather than trusting the browser’s option list. For a numeric limit, convert the value and clamp it to the range your query or markup can handle.

Separate data retrieval from presentation when the widget becomes substantial. Fetch the data in a small method, then keep widget() responsible for wrappers, escaping, and output. This makes it easier to test empty results and avoids mixing database logic with HTML. If a widget performs expensive remote or database work, use an appropriate cache strategy and fail gracefully when no data is available; do not leave an unclosed theme wrapper on the page.

Keep the base ID and saved field names stable across releases. Renaming a class is safe when the string passed to register_widget() still resolves to it, but changing the widget’s internal ID or setting keys requires a migration plan if existing sites must retain their configurations.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Answering the essential development questions

How do I create a custom WordPress widget?

Extend WP_Widget, call the parent constructor, override widget(), add form() and update() when configuration is needed, and register the class on widgets_init. Then place an instance in a widget area supplied by the active theme.

Which methods does WP_Widget require?

The rendering contract centers on widget($args, $instance), which must be overridden to output the front-end content. form($instance) supplies administrator controls, and update($new_instance, $old_instance) validates and sanitizes saved settings when the widget is configurable.

How does a custom widget work with the block editor?

A classic PHP widget remains a WP_Widget class and can be exposed through the editor’s legacy-widget interface. Native block content follows the block-editor registration and rendering path. Core’s WP_Widget_Block, introduced in WordPress 5.8.0, connects block content with widget areas; it does not eliminate the classic API.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More quests from Patch Notes

  1. How to Set Up a RedM (RDR2) Server in 2026: License Key, txAdmin and server.cfg, Step by StepBlog11min
  2. Best RedM (RDR2) Server Hosting in 2026: Comparing Four Hosts on Slots, Memory and PriceBlog10min
  3. How to Host a Mindustry Server in 2026: server-release.jar, Port 6567 and the Commands That MatterBlog7min
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.