Widget Class Generator
All four methods, wired to your fields: the output, the admin form, and an update() that sanitises each value by its own type.
<?php // Generated with WP CodeKit — powered by GrowQuest (https://growquest.io). /** * Plugin Name: Recent Case Studies * Description: Adds the Recent Case Studies widget. * Version: 1.0.0 * Requires PHP: 7.4 * Text Domain: acme */ defined( 'ABSPATH' ) || exit; /** * Recent Case Studies. */ class Acme_Case_Studies_Widget extends WP_Widget { /** * Register the widget with its id base and picker labels. */ public function __construct() { parent::__construct( 'acme_case_studies', __( 'Recent Case Studies', 'acme' ), array( 'description' => __( 'Lists the newest case studies with links.', 'acme' ), 'classname' => 'acme-case-studies', ) ); } /** * Default values for every field. * * @return array */ protected function defaults() { return array( 'title' => 'Recent work', 'count' => 5, 'order' => 'date', 'show_date' => false, ); } /** * Front-end output. * * @param array $args Sidebar arguments. * @param array $instance Saved settings. */ public function widget( $args, $instance ) { $instance = wp_parse_args( (array) $instance, $this->defaults() ); echo $args['before_widget']; if ( ! empty( $instance['title'] ) ) { echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ) ) . $args['after_title']; } $posts = get_posts( array( 'post_type' => 'post', 'posts_per_page' => (int) $instance['count'], 'post_status' => 'publish', 'no_found_rows' => true, ) ); if ( $posts ) { echo '<ul>'; foreach ( $posts as $post ) { printf( '<li><a href="%1$s">%2$s</a></li>', esc_url( get_permalink( $post ) ), esc_html( get_the_title( $post ) ) ); } echo '</ul>'; } echo $args['after_widget']; } /** * The settings form in the admin. * * @param array $instance Saved settings. */ public function form( $instance ) { $instance = wp_parse_args( (array) $instance, $this->defaults() ); printf( '<p><label for="%1$s">%3$s</label><input class="widefat" type="text" id="%1$s" name="%2$s" value="%4$s" /></p>', esc_attr( $this->get_field_id( 'title' ) ), esc_attr( $this->get_field_name( 'title' ) ), esc_html( __( 'Title', 'acme' ) ), esc_attr( $instance['title'] ) ); printf( '<p><label for="%1$s">%3$s</label><input class="widefat" type="number" id="%1$s" name="%2$s" value="%4$s" /></p>', esc_attr( $this->get_field_id( 'count' ) ), esc_attr( $this->get_field_name( 'count' ) ), esc_html( __( 'How many to show', 'acme' ) ), esc_attr( $instance['count'] ) ); printf( '<p><label for="%1$s">%3$s</label>', esc_attr( $this->get_field_id( 'order' ) ), esc_attr( $this->get_field_name( 'order' ) ), esc_html( __( 'Order', 'acme' ) ) ); printf( '<select class="widefat" id="%1$s" name="%2$s">', esc_attr( $this->get_field_id( 'order' ) ), esc_attr( $this->get_field_name( 'order' ) ) ); foreach ( array( 'date' => __( 'Newest first', 'acme' ), 'title' => __( 'A to Z', 'acme' ), ) as $value => $label ) { printf( '<option value="%1$s"%2$s>%3$s</option>', esc_attr( $value ), selected( $instance['order'], $value, false ), esc_html( $label ) ); } echo '</select></p>'; printf( '<p><input class="checkbox" type="checkbox" id="%1$s" name="%2$s" value="1"%3$s /> <label for="%1$s">%4$s</label></p>', esc_attr( $this->get_field_id( 'show_date' ) ), esc_attr( $this->get_field_name( 'show_date' ) ), checked( (bool) $instance['show_date'], true, false ), esc_html( __( 'Show the date', 'acme' ) ) ); } /** * Sanitise settings on save. * * @param array $new_instance Submitted values. * @param array $old_instance Previously saved values. * @return array */ public function update( $new_instance, $old_instance ) { $instance = $old_instance; $instance['title'] = isset( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : 'Recent work'; $instance['count'] = isset( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 5; $instance['order'] = isset( $new_instance['order'] ) && in_array( sanitize_key( $new_instance['order'] ), array( 'date', 'title' ), true ) ? sanitize_key( $new_instance['order'] ) : 'date'; $instance['show_date'] = ! empty( $new_instance['show_date'] ); return $instance; } } /** * Register the widget. */ function acme_case_studies_register() { register_widget( 'Acme_Case_Studies_Widget' ); } add_action( 'widgets_init', 'acme_case_studies_register' );
About this generator
WordPress Custom Widget Generator Online
Describe the settings your widget needs and get the whole WP_Widget subclass — __construct(), defaults(), widget(), form() and update() — with per-field sanitisation, escaped output and the register_widget() call already hooked to widgets_init. This is the WordPress widget class boilerplate written out properly rather than a stub with four empty methods.
A live mock of the Appearance › Widgets panel shows the admin form your field list produces, so you can see the labels, the select options and the checkbox before you paste anything. Free, no account, and nothing you type leaves the browser.
Reviewed Output tested on WordPress 6.8 · PHP 8.3
Why the widget class generator beats a copied WP_Widget skeleton
The skeletons you find online stop at method signatures. Everything that matters is in the bodies: get_field_id() and get_field_name() so core can namespace each input per instance, a different sanitiser for every field type in update(), wp_parse_args() against real defaults so a new field does not throw notices on an existing instance, and printing all four of before_widget, after_widget, before_title and after_title. This generator writes those bodies from your field list.
Six field types, each with the right sanitiser
Text uses sanitize_text_field(), textarea wp_kses_post(), number absint(), URL esc_url_raw(), select sanitize_key() against a whitelist of your own choices, and checkbox ! empty(). Nothing falls through to an unsanitised assignment.
Admin markup that core can namespace
Every input is printed through printf() with esc_attr( $this->get_field_id( … ) ) and esc_attr( $this->get_field_name( … ) ), which is what keeps two instances of the same widget from overwriting each other's values.
Four ready-made widget() bodies
Recent posts (a get_posts() call with no_found_rows, driven by your number field, printed as an escaped list), rich text (through wpautop() and wp_kses_post()), a custom list with a get_items() stub you fill in, or an empty body between the sidebar wrappers.
The id_base warning you want before release, not after
The id_base keys every saved instance in wp_options. Changing it later orphans every widget a site owner has already placed, so an unsafe value is flagged with the corrected form shown, and duplicate field ids are errors for the same reason.
Checks tied to the body you picked
A recent-posts body with no number field means the count is hard-coded at five. A rich-text body with no textarea has nothing to render. A select with no choices always falls back to its default. Each is flagged with a one-click fix that adds the missing field.
An optional shortcode wrapper
The same output as a shortcode for pages that are not sidebars, using ob_start() around widget() with sensible wrapper markup — with the honest note that it uses the passed attributes rather than any saved instance.
How does the Widget Class Generator work?
Four steps, and the admin preview redraws after each one so the form you are describing is visible the whole time.
- 01
Name the widget
Set the picker name, the description under it, the
id_base, the PHP class name and the text domain. The class name is checked against the Capitalised_With_Underscores convention. - 02
Choose what it renders
Recent posts, rich text, a custom list or an empty stub. The choice decides what goes into
widget()and which fields the Checks tab then expects you to have. - 03
Add the settings fields
Give each field an id, a label, a type and a default; selects take their choices as a
value:Labellist. Reorder by dragging — the order here is the order in the admin form. - 04
Clear the checks, then export
Resolve the flagged ids and missing fields, optionally turn on the shortcode wrapper, then copy the class or download it as a snippet, a
functions.phpblock or a plugin file.
Worked example — the head of a Recent Case Studies widget
The class opening and the registration, exactly as generated. defaults(), widget(), form() and update() follow in the full output — one branch per field in each.
/**
* Recent Case Studies.
*/
class Acme_Case_Studies_Widget extends WP_Widget {
/**
* Register the widget with its id base and picker labels.
*/
public function __construct() {
parent::__construct(
'acme_case_studies',
__( 'Recent Case Studies', 'acme' ),
array(
'description' => __( 'Lists the newest case studies with links.', 'acme' ),
'classname' => 'acme-case-studies',
)
);
}
}
/**
* Register the widget.
*/
function acme_case_studies_register() {
register_widget( 'Acme_Case_Studies_Widget' );
}
add_action( 'widgets_init', 'acme_case_studies_register' );For the same four fields, update() comes out as sanitize_text_field() on the title, absint() on the count, a sanitize_key() whitelist check on the select and ! empty() on the checkbox — each falling back to that field's declared default.
Custom widgets — frequently asked questions
The questions that come up when a WP_Widget subclass does not behave the way the handbook implies.
Why does my custom widget not appear in the widget picker?
register_widget() has to run inside a callback hooked to widgets_init, and the class must already be defined when it runs. If the class lives in a separate file, require it before the hook fires. A fatal error anywhere earlier in functions.php will also stop the registration silently, so check the PHP error log before assuming the widget code is wrong.
Why do two copies of my widget share the same settings?
The form is printing hard-coded id and name attributes instead of $this->get_field_id( 'key' ) and $this->get_field_name( 'key' ). Those methods namespace each input with the instance number, which is the only thing that keeps two placements of the same widget separate. Without them both instances post to the same field name and the last one saved wins.
Should I still build WP_Widget widgets, or a block?
Both still work. Since WordPress 5.8 the Widgets screen is block-based and a WP_Widget subclass renders inside a Legacy Widget block — nothing has been removed. For a classic theme with real widget areas, a widget class is still the pragmatic choice and takes far less tooling. For a block theme, where the site editor and template parts replace widget areas entirely, write a block instead.
Why does my widget show a "no preview available" message in the block editor?
The Legacy Widget block can only render a live preview when the widget's instance data is exposed to the REST API. Pass 'show_instance_in_rest' => true in the options array of your parent::__construct() call. It is opt-in because core cannot know whether your instance array is safe to expose; only enable it if the saved settings contain nothing sensitive.
Where does the widget title heading markup come from?
From the sidebar, not the widget. $args['before_title'] and $args['after_title'] are whatever the theme passed to register_sidebar(), which is why a widget should echo them around the title rather than printing its own <h2>. Run the title through apply_filters( 'widget_title', … ) as well, so plugins that translate or modify widget titles keep working.
Do I need to sanitise in update() if I escape in widget()?
Yes, both. update() decides what is written to the database and is the only place the raw submitted value is seen, so unsanitised input is stored permanently. Escaping in widget() protects the specific context you are printing into — esc_html() for text, esc_url() for a link, esc_attr() for an attribute. Sanitise on input, escape on output; neither replaces the other.
Where do I paste the code from the Widget Class generator?
You have three good options and one bad one. Best is a small site-specific plugin — the generator can output one for you, and it keeps working when you change theme. A code-snippets plugin is fine too. A child theme's functions.php works but ties the code to that theme. Never edit the parent theme: the next update overwrites it and your code is gone.
Can I use the generated code in client work or a commercial plugin?
Yes. The output is ordinary WordPress API calls that you configured — there is no licence attached to it and no attribution required. Use it in client sites, commercial plugins and themes, or products you sell. The generated file carries a short credit comment you are free to delete.
The other Design generators in the library, plus the WordPress tools most often used alongside this one.