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_Widgetclass. 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 useget_field_id()andget_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. Runsanitize_text_field(),sanitize_textarea_field(), or a type-specific check on every field. - Hardcoding field IDs. Use
get_field_id()andget_field_name(). Hardcoded IDs break the moment a user adds a second copy of the widget. - Registering on the wrong hook.
register_widget()belongs onwidgets_init, notinit.register_block_type()for a block belongs oninit. - Editing the parent theme. Adding widget code to a parent theme’s
functions.phpmeans 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:
- The widget appears in Appearance > Widgets (or the block inserter for a Legacy Widget).
- Adding it to a widget area and saving works without an error.
- The settings form shows saved values correctly after a page reload.
- Two copies of the widget on the same page keep separate settings.
- The front end renders the saved values and escapes them.
- With
WP_DEBUGon, 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.