Learning Objectives
- Understand WP_Widget class structure
- Create custom widget classes
- Implement widget() method for output
- Build form() method for admin controls
- Handle update() method for saving data
- Register custom widgets properly
- Add JavaScript and CSS to widgets
- Create complex widget controls
Understanding WP_Widget Class
The WP_Widget class is the foundation for creating custom widgets in WordPress. By extending this class, you can create powerful, reusable widgets with admin controls.
Key Concept
Creating a Basic Custom Widget
Simple Custom Widget Class
<?php
/**
* Custom Welcome Widget
*/
class MyTheme_Welcome_Widget extends WP_Widget {
/**
* Constructor
*/
public function __construct() {
$widget_options = array(
'classname' => 'mytheme_welcome_widget',
'description' => __( 'Display a welcome message with custom title and content.', 'mytheme' ),
'customize_selective_refresh' => true,
);
parent::__construct(
'mytheme_welcome', // Widget ID
__( 'Welcome Message', 'mytheme' ), // Widget name
$widget_options // Widget options
);
}
/**
* Output the widget content
*/
public function widget( $args, $instance ) {
// Extract widget arguments
extract( $args );
// Get widget fields
$title = apply_filters( 'widget_title',
empty( $instance['title'] ) ? '' : $instance['title'],
$instance,
$this->id_base
);
$content = ! empty( $instance['content'] ) ? $instance['content'] : '';
$show_date = ! empty( $instance['show_date'] ) ? true : false;
// Output before widget
echo $before_widget;
// Output title
if ( ! empty( $title ) ) {
echo $before_title . $title . $after_title;
}
// Output content
?>
<div class="welcome-widget-content">
<?php if ( $content ) : ?>
<div class="welcome-message">
<?php echo wpautop( esc_html( $content ) ); ?>
</div>
<?php endif; ?>
<?php if ( $show_date ) : ?>
<p class="welcome-date">
<?php echo esc_html( date_i18n( get_option( 'date_format' ) ) ); ?>
</p>
<?php endif; ?>
</div>
<?php
// Output after widget
echo $after_widget;
}
/**
* Output the widget form in admin
*/
public function form( $instance ) {
// Get current values
$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
$content = ! empty( $instance['content'] ) ? $instance['content'] : '';
$show_date = ! empty( $instance['show_date'] ) ? (bool) $instance['show_date'] : false;
?>
<!-- Title Field -->
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
<?php esc_html_e( 'Title:', 'mytheme' ); ?>
</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>
<!-- Content Field -->
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'content' ) ); ?>">
<?php esc_html_e( 'Content:', 'mytheme' ); ?>
</label>
<textarea
class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'content' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'content' ) ); ?>"
rows="5"
><?php echo esc_textarea( $content ); ?></textarea>
</p>
<!-- Show Date Checkbox -->
<p>
<input
class="checkbox"
type="checkbox"
id="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_date' ) ); ?>"
<?php checked( $show_date ); ?>
/>
<label for="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>">
<?php esc_html_e( 'Display current date?', 'mytheme' ); ?>
</label>
</p>
<?php
}
/**
* Update widget settings
*/
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = ( ! empty( $new_instance['title'] ) )
? sanitize_text_field( $new_instance['title'] )
: '';
$instance['content'] = ( ! empty( $new_instance['content'] ) )
? sanitize_textarea_field( $new_instance['content'] )
: '';
$instance['show_date'] = ( ! empty( $new_instance['show_date'] ) )
? true
: false;
return $instance;
}
}
/**
* Register the widget
*/
function mytheme_register_welcome_widget() {
register_widget( 'MyTheme_Welcome_Widget' );
}
add_action( 'widgets_init', 'mytheme_register_welcome_widget' );
WP_Widget Methods Reference
| Method | Purpose | When Called |
|---|---|---|
__construct() |
Initialize widget properties | Widget instantiation |
widget() |
Output widget HTML | Frontend display |
form() |
Display admin form | Widget admin panel |
update() |
Save widget settings | Form submission |
get_field_id() |
Get field HTML id | Form creation |
get_field_name() |
Get field HTML name | Form creation |
Advanced Widget Examples
Recent Posts Widget with Thumbnails
<?php
/**
* Custom Recent Posts Widget
*/
class MyTheme_Recent_Posts_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'mytheme_recent_posts',
__( 'Recent Posts with Thumbnails', 'mytheme' ),
array(
'description' => __( 'Display recent posts with thumbnails and excerpts.', 'mytheme' ),
'customize_selective_refresh' => true,
)
);
// Add widget styles
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
}
public function enqueue_styles() {
if ( is_active_widget( false, false, $this->id_base, true ) ) {
wp_enqueue_style(
'mytheme-recent-posts-widget',
get_template_directory_uri() . '/css/widgets/recent-posts.css',
array(),
'1.0.0'
);
}
}
public function widget( $args, $instance ) {
extract( $args );
$title = apply_filters( 'widget_title',
empty( $instance['title'] ) ? __( 'Recent Posts', 'mytheme' ) : $instance['title'],
$instance,
$this->id_base
);
$number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : 5;
$show_date = ! empty( $instance['show_date'] ) ? true : false;
$show_thumbnail = ! empty( $instance['show_thumbnail'] ) ? true : false;
$show_excerpt = ! empty( $instance['show_excerpt'] ) ? true : false;
$category = ! empty( $instance['category'] ) ? absint( $instance['category'] ) : 0;
// Query arguments
$query_args = array(
'posts_per_page' => $number,
'post_status' => 'publish',
'ignore_sticky_posts' => true,
);
if ( $category ) {
$query_args['cat'] = $category;
}
$recent_posts = new WP_Query( $query_args );
if ( $recent_posts->have_posts() ) :
echo $before_widget;
if ( $title ) {
echo $before_title . $title . $after_title;
}
?>
<ul class="mytheme-recent-posts">
<?php while ( $recent_posts->have_posts() ) : $recent_posts->the_post(); ?>
<li class="recent-post-item">
<?php if ( $show_thumbnail && has_post_thumbnail() ) : ?>
<div class="post-thumbnail">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail( 'thumbnail' ); ?>
</a>
</div>
<?php endif; ?>
<div class="post-content">
<h4 class="post-title">
<a href="<?php the_permalink(); ?>">
<?php get_the_title() ? the_title() : the_ID(); ?>
</a>
</h4>
<?php if ( $show_date ) : ?>
<time class="post-date" datetime="<?php echo get_the_date( 'c' ); ?>">
<?php echo get_the_date(); ?>
</time>
<?php endif; ?>
<?php if ( $show_excerpt ) : ?>
<div class="post-excerpt">
<?php echo wp_trim_words( get_the_excerpt(), 15 ); ?>
</div>
<?php endif; ?>
</div>
</li>
<?php endwhile; ?>
</ul>
<?php
echo $after_widget;
endif;
wp_reset_postdata();
}
public function form( $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
$number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : 5;
$show_date = ! empty( $instance['show_date'] ) ? (bool) $instance['show_date'] : false;
$show_thumbnail = ! empty( $instance['show_thumbnail'] ) ? (bool) $instance['show_thumbnail'] : true;
$show_excerpt = ! empty( $instance['show_excerpt'] ) ? (bool) $instance['show_excerpt'] : false;
$category = ! empty( $instance['category'] ) ? absint( $instance['category'] ) : 0;
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>">
<?php _e( 'Title:', 'mytheme' ); ?>
</label>
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>"
name="<?php echo $this->get_field_name( 'title' ); ?>" type="text"
value="<?php echo esc_attr( $title ); ?>" />
</p>
<p>
<label for="<?php echo $this->get_field_id( 'number' ); ?>">
<?php _e( 'Number of posts:', 'mytheme' ); ?>
</label>
<input class="tiny-text" id="<?php echo $this->get_field_id( 'number' ); ?>"
name="<?php echo $this->get_field_name( 'number' ); ?>" type="number"
step="1" min="1" value="<?php echo $number; ?>" size="3" />
</p>
<p>
<label for="<?php echo $this->get_field_id( 'category' ); ?>">
<?php _e( 'Category:', 'mytheme' ); ?>
</label>
<?php wp_dropdown_categories( array(
'show_option_all' => __( 'All Categories', 'mytheme' ),
'orderby' => 'name',
'order' => 'ASC',
'show_count' => true,
'hide_empty' => false,
'selected' => $category,
'hierarchical' => true,
'name' => $this->get_field_name( 'category' ),
'id' => $this->get_field_id( 'category' ),
'class' => 'widefat',
) ); ?>
</p>
<p>
<input class="checkbox" type="checkbox"
id="<?php echo $this->get_field_id( 'show_date' ); ?>"
name="<?php echo $this->get_field_name( 'show_date' ); ?>"
<?php checked( $show_date ); ?> />
<label for="<?php echo $this->get_field_id( 'show_date' ); ?>">
<?php _e( 'Display post date?', 'mytheme' ); ?>
</label>
</p>
<p>
<input class="checkbox" type="checkbox"
id="<?php echo $this->get_field_id( 'show_thumbnail' ); ?>"
name="<?php echo $this->get_field_name( 'show_thumbnail' ); ?>"
<?php checked( $show_thumbnail ); ?> />
<label for="<?php echo $this->get_field_id( 'show_thumbnail' ); ?>">
<?php _e( 'Display thumbnails?', 'mytheme' ); ?>
</label>
</p>
<p>
<input class="checkbox" type="checkbox"
id="<?php echo $this->get_field_id( 'show_excerpt' ); ?>"
name="<?php echo $this->get_field_name( 'show_excerpt' ); ?>"
<?php checked( $show_excerpt ); ?> />
<label for="<?php echo $this->get_field_id( 'show_excerpt' ); ?>">
<?php _e( 'Display excerpt?', 'mytheme' ); ?>
</label>
</p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = sanitize_text_field( $new_instance['title'] );
$instance['number'] = absint( $new_instance['number'] );
$instance['show_date'] = ! empty( $new_instance['show_date'] ) ? true : false;
$instance['show_thumbnail'] = ! empty( $new_instance['show_thumbnail'] ) ? true : false;
$instance['show_excerpt'] = ! empty( $new_instance['show_excerpt'] ) ? true : false;
$instance['category'] = absint( $new_instance['category'] );
return $instance;
}
}
Social Links Widget
<?php
/**
* Social Links Widget
*/
class MyTheme_Social_Links_Widget extends WP_Widget {
private $social_networks = array(
'facebook' => 'Facebook',
'twitter' => 'Twitter',
'instagram' => 'Instagram',
'linkedin' => 'LinkedIn',
'youtube' => 'YouTube',
'pinterest' => 'Pinterest',
'github' => 'GitHub',
);
public function __construct() {
parent::__construct(
'mytheme_social_links',
__( 'Social Links', 'mytheme' ),
array(
'description' => __( 'Display social media links with icons.', 'mytheme' ),
'customize_selective_refresh' => true,
)
);
}
public function widget( $args, $instance ) {
extract( $args );
$title = apply_filters( 'widget_title',
empty( $instance['title'] ) ? '' : $instance['title'],
$instance,
$this->id_base
);
echo $before_widget;
if ( $title ) {
echo $before_title . $title . $after_title;
}
?>
<div class="social-links-widget">
<ul class="social-links">
<?php foreach ( $this->social_networks as $network => $label ) : ?>
<?php if ( ! empty( $instance[ $network ] ) ) : ?>
<li class="social-link social-<?php echo esc_attr( $network ); ?>">
<a href="<?php echo esc_url( $instance[ $network ] ); ?>"
target="_blank"
rel="noopener noreferrer"
aria-label="<?php echo esc_attr( $label ); ?>">
<span class="dashicons dashicons-<?php echo esc_attr( $network ); ?>"></span>
<span class="screen-reader-text"><?php echo esc_html( $label ); ?></span>
</a>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
</div>
<?php
echo $after_widget;
}
public function form( $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>">
<?php _e( 'Title:', 'mytheme' ); ?>
</label>
<input class="widefat"
id="<?php echo $this->get_field_id( 'title' ); ?>"
name="<?php echo $this->get_field_name( 'title' ); ?>"
type="text"
value="<?php echo esc_attr( $title ); ?>" />
</p>
<?php foreach ( $this->social_networks as $network => $label ) : ?>
<p>
<label for="<?php echo $this->get_field_id( $network ); ?>">
<?php echo esc_html( $label ); ?> URL:
</label>
<input class="widefat"
id="<?php echo $this->get_field_id( $network ); ?>"
name="<?php echo $this->get_field_name( $network ); ?>"
type="url"
value="<?php echo esc_url( ! empty( $instance[ $network ] ) ? $instance[ $network ] : '' ); ?>"
placeholder="https://<?php echo esc_attr( $network ); ?>.com/username" />
</p>
<?php endforeach; ?>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = sanitize_text_field( $new_instance['title'] );
foreach ( $this->social_networks as $network => $label ) {
$instance[ $network ] = ! empty( $new_instance[ $network ] )
? esc_url_raw( $new_instance[ $network ] )
: '';
}
return $instance;
}
}
Widget with JavaScript Integration
AJAX-Powered Widget
<?php
/**
* AJAX Content Loader Widget
*/
class MyTheme_AJAX_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'mytheme_ajax_widget',
__( 'AJAX Content Loader', 'mytheme' ),
array(
'description' => __( 'Load content dynamically via AJAX.', 'mytheme' ),
)
);
// Register AJAX handlers
add_action( 'wp_ajax_mytheme_load_widget_content', array( $this, 'ajax_load_content' ) );
add_action( 'wp_ajax_nopriv_mytheme_load_widget_content', array( $this, 'ajax_load_content' ) );
// Enqueue scripts
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
}
public function enqueue_scripts() {
if ( is_active_widget( false, false, $this->id_base, true ) ) {
wp_enqueue_script(
'mytheme-ajax-widget',
get_template_directory_uri() . '/js/widgets/ajax-widget.js',
array( 'jquery' ),
'1.0.0',
true
);
wp_localize_script( 'mytheme-ajax-widget', 'mytheme_ajax', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mytheme_ajax_widget' ),
) );
}
}
public function widget( $args, $instance ) {
extract( $args );
$title = apply_filters( 'widget_title',
empty( $instance['title'] ) ? '' : $instance['title'],
$instance,
$this->id_base
);
$content_type = ! empty( $instance['content_type'] ) ? $instance['content_type'] : 'posts';
echo $before_widget;
if ( $title ) {
echo $before_title . $title . $after_title;
}
?>
<div class="ajax-widget-container" data-content-type="<?php echo esc_attr( $content_type ); ?>">
<div class="ajax-content">
<p><?php _e( 'Loading...', 'mytheme' ); ?></p>
</div>
<button class="ajax-load-more" style="display: none;">
<?php _e( 'Load More', 'mytheme' ); ?>
</button>
</div>
<?php
echo $after_widget;
}
public function ajax_load_content() {
// Verify nonce
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'mytheme_ajax_widget' ) ) {
wp_die( 'Security check failed' );
}
$content_type = isset( $_POST['content_type'] ) ? sanitize_text_field( $_POST['content_type'] ) : 'posts';
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
$args = array(
'posts_per_page' => 3,
'paged' => $page,
'post_status' => 'publish',
);
if ( $content_type === 'pages' ) {
$args['post_type'] = 'page';
}
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
?>
<div class="ajax-item">
<h4><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h4>
<p><?php echo wp_trim_words( get_the_excerpt(), 20 ); ?></p>
</div>
<?php
}
if ( $query->max_num_pages > $page ) {
echo '<span class="has-more" data-max-pages="' . $query->max_num_pages . '"></span>';
}
} else {
echo '<p>' . __( 'No content found.', 'mytheme' ) . '</p>';
}
wp_reset_postdata();
wp_die();
}
public function form( $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
$content_type = ! empty( $instance['content_type'] ) ? $instance['content_type'] : 'posts';
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>">
<?php _e( 'Title:', 'mytheme' ); ?>
</label>
<input class="widefat"
id="<?php echo $this->get_field_id( 'title' ); ?>"
name="<?php echo $this->get_field_name( 'title' ); ?>"
type="text"
value="<?php echo esc_attr( $title ); ?>" />
</p>
<p>
<label for="<?php echo $this->get_field_id( 'content_type' ); ?>">
<?php _e( 'Content Type:', 'mytheme' ); ?>
</label>
<select class="widefat"
id="<?php echo $this->get_field_id( 'content_type' ); ?>"
name="<?php echo $this->get_field_name( 'content_type' ); ?>">
<option value="posts" <?php selected( $content_type, 'posts' ); ?>>
<?php _e( 'Posts', 'mytheme' ); ?>
</option>
<option value="pages" <?php selected( $content_type, 'pages' ); ?>>
<?php _e( 'Pages', 'mytheme' ); ?>
</option>
</select>
</p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = sanitize_text_field( $new_instance['title'] );
$instance['content_type'] = sanitize_text_field( $new_instance['content_type'] );
return $instance;
}
}
// JavaScript file (ajax-widget.js)
/*
jQuery(document).ready(function($) {
$('.ajax-widget-container').each(function() {
var container = $(this);
var contentDiv = container.find('.ajax-content');
var loadMoreBtn = container.find('.ajax-load-more');
var contentType = container.data('content-type');
var currentPage = 1;
// Initial load
loadContent();
// Load more button click
loadMoreBtn.on('click', function() {
currentPage++;
loadContent(true);
});
function loadContent(append = false) {
$.ajax({
url: mytheme_ajax.ajax_url,
type: 'POST',
data: {
action: 'mytheme_load_widget_content',
nonce: mytheme_ajax.nonce,
content_type: contentType,
page: currentPage
},
success: function(response) {
if (append) {
contentDiv.append(response);
} else {
contentDiv.html(response);
}
if (contentDiv.find('.has-more').length) {
loadMoreBtn.show();
} else {
loadMoreBtn.hide();
}
contentDiv.find('.has-more').remove();
}
});
}
});
});
*/
Common Widget Types
Best Practices
Widget Development Best Practices
- Sanitize all input: Use appropriate sanitization functions
- Escape output: Always escape data before display
- Use unique IDs: Prefix widget IDs to avoid conflicts
- Add selective refresh: Enable Customizer live preview
- Enqueue assets conditionally: Only load when widget is active
- Provide defaults: Set sensible default values
- Add widget description: Help users understand widget purpose
- Test thoroughly: Check in different widget areas
Always validate and sanitize user input in the update() method to prevent security vulnerabilities. Never trust data coming from form submissions.
Practice Exercise
Create Custom Widget Collection