Extending WordPress Core (or other 3rd-party) Widgets

How long have you been craving for a text size setting on the default Text widget, or a background color on the Calendar widget? Did you know you could implement these yourself? And without modifying the core files? And without jeopardizing the update-ability of your website?

Yup! It can be done! Of course, some things are more easily done, and some things may be impossible to do at all. Let’s see however what is the general approach of actually doing it.

Let’s start by creating a plugin that our code will live into. I will attempt to add a font size setting which will change the widget’s title size, so let’s name the plugin “Widgets Font Size”. Inside wp-admin/plugins/ create a new folder called widgets-font-size and in there create a new PHP file, widgets-font-size.php with the following contents:

<?php
/*
Plugin Name: Widgets Font Size
Plugin URI: https://www.cssigniter.com/extending-wordpress-core-or-other-3rd-party-widgets/
Description: Example on adding a custom setting on third-party widgets.
Version: 1.0
Author: Anastis Sourgoutsidis
Author URI: https://www.cssigniter.com/
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
*/

class Widgets_Font_Size {
	function __construct() {
		// Hook in all the right places.
	}
}

new Widgets_Font_Size();
widgets-font-size.php

Then, go ahead and activate the new plugin from your dashboard. It will do nothing, but everything great starts small!

If you don’t know what the above code does, then perhaps you need to do some reading before continuing with this article. The PHP documentation covers the basics of Object Oriented Programming, and the WordPress Codex walks you through on writing a plugin from scratch. We won’t be using any advanced OOP techniques or features however; we’ll only use a class to avoid prefixing all our functions. Yes, I’m lazy.

It’s otherwise a pretty standard plugin file. It includes a commented header with the necessary information that enables WordPress to recognize it as a plugin, and creates and instantiates the class Widgets_Font_Size. Once activated, we can simply add code inside the class and it will be executed on the next page refresh.

Default values

Our code will be manipulating a lot the widgets’ $instance, which isn’t an object instance, but an array of values of a widget’s instance. Instead of always checking whether a specific setting exists in the $instance variable, we can wp_parse_args() it with our defaults on the beginning of our functions, and then continue assuming the appropriate array indexes are there.

So, let’s declare a $defaults property (a variable that belongs to a class) right before the constructor. It will be an array with our setting’s name as the array key and the default value as the array value. Let’s say the setting will be named title_size, and the default value will be an empty string. Only widgets where title_size is set to a number will be affected by our code.

protected $defaults = array(
	'title_size' => '',
);

Adding a setting in the form

The Widgets API provides only one action related to the widgets’ form (the interface you see when configuring/customizing widgets), and that is ‘in_widget_form’. It fires after the widget’s form callback is finished, which really means we can only append stuff on the bottom of the widget’s form. Somewhere is better than nowhere. Let’s hook on this action:

function __construct() {
	// Hook in all the right places.
	add_action( 'in_widget_form', array( $this, 'add_settings' ), 10, 3 );
}

/**
 * Adds the custom settings to all widgets' forms.
 *
 * @param WP_Widget $widget   An instance of a WP_Widget derived subclass.
 * @param mixed     $return   Return null if new fields are added.
 * @param array     $instance An array of the widget's settings.
 */
public function add_settings( $widget, $return, $instance ) {
	// Make sure $instance contains at least our default values.
	$instance = wp_parse_args( $instance, $this->defaults );
	?>
	<p>
		<label for="<?php echo esc_attr( $widget->get_field_id( 'title_size' ) ); ?>">
			<?php esc_html_e( 'Title Size:', 'wfs' ); ?>
		</label>
		<input id="<?php echo esc_attr( $widget->get_field_id( 'title_size' ) ); ?>"
		       name="<?php echo esc_attr( $widget->get_field_name( 'title_size' ) ); ?>"
		       value="<?php echo esc_attr( $instance['title_size'] ); ?>"
		       type="number"
		       class="widefat"
		/>
	</p>
	<?php
}

The three parameters (also documented on the Developer Reference) are:

  • $widget (renamed since $this is reserved) – The widget’s object instance, which is a subclass of WP_Widget. We use some of its methods to add our own form field.
  • $return – The value returned by the widget’s form callback. It should be null when fields are added by the callback. We don’t need this currently.
  • $instance – An array of the widget’s settings and their respective values. We use this array to read our own settings.

If you’ve ever built your own widget, you’ll find that the add_settings() method is very similar to the form() method that you need to implement when building the widget. In fact, we could have named our method form() as well.

Perhaps the most important line inside the add_settings() method is line 15. It takes care of assigning the default value on all $instance arrays, so that any widgets without the title_size setting (i.e. all previously assigned widgets) will not throw an “Undefined index” notice. Other than that, we just use the $widget‘s get_field_name() so that our input will be recognized as part of the widget, and get_field_id() so that the same setting in different widgets will have unique IDs and the labels will keep working properly.

Save the file and go to Appearance → Widgets.

Screenshot of widgets with Title Size option

Image 1

We can see that the Recent Post widget now displays a Title Size setting. In fact, expand any widget and the setting will be there. How cool is that? “Not really cool” you’ll say. “I’ve just set a value, pressed Save, and the setting appears to be empty. You suck!”. Yes. We’re merely displaying the setting. Let’s also save its value!

Saving the setting

In order to save our setting, the Widget API provides us the widget_update_callback filter. Again, this is called after the plugin had a chance to process its own settings, but before they are saved. It’s almost identical to a widget’s update() method, although there are a few more parameters passed.

function __construct() {
	// Hook in all the right places.
	add_action( 'in_widget_form', array( $this, 'add_settings' ), 10, 3 );
	add_filter( 'widget_update_callback', array( $this, 'save_settings' ), 10, 4 );
}

/**
 * Saves the custom settings.
 *
 * @param array     $instance     The current widget instance's settings.
 * @param array     $new_instance Array of new widget settings.
 * @param array     $old_instance Array of old widget settings.
 * @param WP_Widget $object       The current widget instance.
 *
 * @return array The widget instance's settings to get saved.
 */
public function save_settings( $instance, $new_instance, $old_instance, $object ) {
	// Make sure $instance contains at least our default values.
	$instance = wp_parse_args( $instance, $this->defaults );

	// Now check that a value is actually present, and assign it sanitized.
	if ( isset( $new_instance['title_size'] ) ) {
		$instance['title_size'] = intval( $new_instance['title_size'] ) > 0 ? intval( $new_instance['title_size'] ) : '';
	}

	return $instance;
}

The four parameters are pretty much self-explanatory:

  • $instance – The widget’s settings that are going to be saved. This is what is returned by the update() method, where $new_instance values have been sanitized.
  • $new_instance – The widget’s settings, as submitted by its form.
  • $old_instance – The previously saved settings that are going to be overwritten. This is set to an empty array when first assigning a widget to a sidebar.
  • $widget – Like previously, renamed since $this is reserved. The widget’s object instance.

Inside the save_settings() method we, again, make sure our default is in place by running $instance through wp_parse_args(). This will ensure that our default will get saved. Now, lines 22-24 are the meat of this method, as we check that our setting is present in the $new_instance array, i.e. submitted from the widget’s form. If it’s there, and it has a value larger than zero, we replace our default in $instance with this value, converted to an integer. We then return $instance so it gets saved by the API behind the scenes.

Go ahead and refresh the Widgets page, assign a value and press Save. Voila! Our value persists.

But that’s not very useful right now, is it? Let’s see what we can do with the (now saved) setting.

Implementing the setting

Our setting needs to affect the font size of the widget’s title. Naturally, this should be done with CSS, and since we can’t know each widget’s settings before they actually get processed, we’ll need to employ the use of wp_add_inline_style(). You might find our “How to late enqueue inline CSS in WordPress” tutorial useful.

The Widgets API provides us with the widget_display_callback filter. Now, this fires before the widget’s display callback is called, so we have a chance to manipulate the $instance array as needed.

function __construct() {
	// Hook in all the right places.
	add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
	add_action( 'in_widget_form', array( $this, 'add_settings' ), 10, 3 );
	add_filter( 'widget_update_callback', array( $this, 'save_settings' ), 10, 4 );
	add_filter( 'widget_display_callback', array( $this, 'frontend_settings' ), 10, 3 );
}

/**
 * Include our settings in the display callback's instance array.
 *
 * @param array     $instance The current widget instance's settings.
 * @param WP_Widget $widget   The current widget instance.
 * @param array     $args     An array of default widget arguments.
 *
 * @return array
 */
public function frontend_settings( $instance, $widget, $args ) {
	// Make sure $instance contains at least our default values.
	// Widget may be displayed before save_settings() had a change to save our settings.
	$instance = wp_parse_args( $instance, $this->defaults );

	if ( $instance['title_size'] ) {
		$style = sprintf( '#%s .widget-title { font-size: %spx; }' . PHP_EOL, $args['widget_id'], $instance['title_size'] );

		wp_enqueue_style( 'wfs-front' );
		wp_add_inline_style( 'wfs-front', $style );
	}

	return $instance;
}

public function enqueue_scripts() {
	// Register a dummy style handle. We'll use this to enqueue our dynamic styles.
	wp_register_style( 'wfs-front', false );
}

The available parameters are again very familiar:

  • $instance – The widget’s settings that are going to be passed to the display callback.
  • $widget – Like previously, renamed since $this is reserved. The widget’s object instance.
  • $args – Kinda misleading by the WordPress’ own inline documentation, this array contains the sidebar‘s arguments passed to the widget. For example, the sidebar’s before_title, after_title, before_widget and after_widget arguments are included in this array.

On lines 3 and 33-36 we just registered a style without a source file, which will act as the dependency for our dynamic CSS. Line 6 simply hooks our frontend_settings() method to the widget_display_callback filter.

Now, inside frontend_settings() we first make sure our defaults are in place (see the pattern yet?) as the displayed widget may have never been re-saved since the time we activated our little plugin. Line 23 then does a quick check on the value of our setting. If it’s not falsey (i.e. empty string, false, 0, etc) it goes on to construct a CSS statement using the $args[‘widget_id’] as part of the selector, so that only the specific widget instance will be affected. Of course, $instance[‘title_size’] is used as part of a font-size rule in order to change the size.

Last, you might have noticed that wp_enqueue_style() is called right there with wp_add_inline_style(). This is needed as a) there’s no reason to enqueue it beforehand if we don’t have any inline style to add, and b) the dependency ‘wfs-front‘ needs to be printed along with the inline styles; that is, inline styles added before the wp_head action need to have their dependent style handle also enqueued before the wp_head action. Same thing goes for the wp_footer action. Since widgets are processed way after the wp_head action, we need to enqueue ‘wfs-front‘ later as well.

So, go ahead, set a title size to one of your widgets and visit your website. This is what I see on the blog of a local installation with our newest theme, Benson:

Screenshot of widgets with different title size

Image 2

Hooray! It works!

But what if we only want this setting to apply on specific widgets? Or if we want it to apply on all widgets except some?

Targeting specific widgets

It’s actually very easy to make our setting appear, save, and work, only on specific widgets. All we really need to do is create a list of widget IDs (id_base actually) that we want to whitelist (or blacklist), and check against this list before we do anything inside our hooks. In order to determine the id_base of a widget, we need to inspect its source code. Luckily, WordPress and its ecosystem are open source, so we can access the source code easily. Let’s open the file wp-includes/widgets/class-wp-widget-recent-posts.php and inspect its constructor:

public function __construct() {
	$widget_ops = array(
		'classname' => 'widget_recent_entries',
		'description' => __( 'Your site’s most recent Posts.' ),
		'customize_selective_refresh' => true,
	);
	parent::__construct( 'recent-posts', __( 'Recent Posts' ), $widget_ops );
	$this->alt_option_name = 'widget_recent_entries';
}
WP_Widget_Recent_Posts constructor

On line 7, the first argument of parent::__contruct() is ‘recent-posts’. This is the id_base of the widget. Each widget assigned gets a number suffixed, hence creating a unique widget ID, e.g. ‘recent-posts-2‘. We are only interested in the first part of the ID however.

Now that we have what we need, let’s create a property in our class that will hold the id_base of the widgets we want to whitelist.

protected $widgets = array(
	'recent-posts',
);

Let’s also create a helper method, that will return true or false, depending if the specific widget is supported or not:

protected function is_supported( WP_Widget $widget ) {
	if ( in_array( $widget->id_base, $this->widgets, true ) ) {
		return true;
	}

	return false;
}

We’ve set the first parameter to be the widget’s object instanc, so that we can check its id_base property. We could expect the property passed to be the id_base itself instead, passing the whole object however, makes calling the method easier, and gives us the freedom to later modify is_supported() to check any of the object’s properties. If we wanted to blacklist the widgets instead, we would just negate ( ! ) the in_array() check on line 2.

Finally, we need to use our helper on each hook, returning early when the widget isn’t supported.

class Widgets_Font_Size {

	// ...

	protected $widgets = array(
		'recent-posts',
	);

	public function add_settings( $widget, $return, $instance ) {
		if ( ! $this->is_supported( $widget ) ) {
			return null;
		}

		// ...
	}

	public function save_settings( $instance, $new_instance, $old_instance, $widget ) {
		if ( ! $this->is_supported( $widget ) ) {
			return $instance;
		}

		// ...
	}

	public function frontend_settings( $instance, $widget, $args ) {
		if ( ! $this->is_supported( $widget ) ) {
			return $instance;
		}

		// ...
	}

	protected function is_supported( WP_Widget $widget ) {
		if ( in_array( $widget->id_base, $this->widgets, true ) ) {
			return true;
		}

		return false;
	}
	
	// ...
}

That should be it. Refresh your widgets page. The Title Size setting should disappear from all widgets except from Recent Posts. This is what my sidebar looks like:

Screenshot of widgets, one of the having a Title Size setting

Image 3

Hooray! Everything works!

All together

Just in case you missed a step, this is the complete code that we ended up with:

<?php
/*
Plugin Name: Widgets Font Size
Plugin URI: https://www.cssigniter.com/extending-wordpress-core-or-other-3rd-party-widgets/
Description: Example on adding a custom setting on third-party widgets.
Version: 1.0
Author: Anastis Sourgoutsidis
Author URI: https://www.cssigniter.com/
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
*/

class Widgets_Font_Size {

	protected $defaults = array(
		'title_size' => '',
	);

	protected $widgets = array(
		'recent-posts',
	);

	function __construct() {
		// Hook in all the right places.
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
		add_action( 'in_widget_form', array( $this, 'add_settings' ), 10, 3 );
		add_filter( 'widget_update_callback', array( $this, 'save_settings' ), 10, 4 );
		add_filter( 'widget_display_callback', array( $this, 'frontend_settings' ), 10, 3 );
	}

	/**
	 * Adds the custom settings to all widgets' forms.
	 *
	 * @param WP_Widget $widget   An instance of a WP_Widget derived subclass.
	 * @param mixed     $return   Return null if new fields are added.
	 * @param array     $instance An array of the widget's settings.
	 */
	public function add_settings( $widget, $return, $instance ) {
		if ( ! $this->is_supported( $widget ) ) {
			return null;
		}

		// Make sure $instance contains at least our default values.
		$instance = wp_parse_args( $instance, $this->defaults );
		?>
		<p>
			<label for="<?php echo esc_attr( $widget->get_field_id( 'title_size' ) ); ?>">
				<?php esc_html_e( 'Title Size:', 'wfs' ); ?>
			</label>
			<input id="<?php echo esc_attr( $widget->get_field_id( 'title_size' ) ); ?>"
			       name="<?php echo esc_attr( $widget->get_field_name( 'title_size' ) ); ?>"
			       value="<?php echo esc_attr( $instance['title_size'] ); ?>"
			       type="number"
			       class="widefat"
			/>
		</p>
		<?php
	}

	/**
	 * Saves the custom settings.
	 *
	 * @param array     $instance     The current widget instance's settings.
	 * @param array     $new_instance Array of new widget settings.
	 * @param array     $old_instance Array of old widget settings.
	 * @param WP_Widget $widget       The current widget instance.
	 *
	 * @return array The widget instance's settings to get saved.
	 */
	public function save_settings( $instance, $new_instance, $old_instance, $widget ) {
		if ( ! $this->is_supported( $widget ) ) {
			return $instance;
		}

		// Make sure $instance contains at least our default values.
		$instance = wp_parse_args( $instance, $this->defaults );

		// Now check that a value is actually present, and assign it sanitized.
		if ( isset( $new_instance['title_size'] ) ) {
			$instance['title_size'] = intval( $new_instance['title_size'] ) > 0 ? intval( $new_instance['title_size'] ) : '';
		}

		return $instance;
	}

	/**
	 * Include our settings in the display callback's instance array.
	 *
	 * @param array     $instance The current widget instance's settings.
	 * @param WP_Widget $widget   The current widget instance.
	 * @param array     $args     An array of default widget arguments.
	 *
	 * @return array
	 */
	public function frontend_settings( $instance, $widget, $args ) {
		if ( ! $this->is_supported( $widget ) ) {
			return $instance;
		}

		// Make sure $instance contains at least our default values.
		// Widget may be displayed before save_settings() had a change to save our settings.
		$instance = wp_parse_args( $instance, $this->defaults );

		if ( $instance['title_size'] ) {
			$style = sprintf( '#%s .widget-title { font-size: %spx; }' . PHP_EOL, $args['widget_id'], $instance['title_size'] );

			wp_enqueue_style( 'wfs-front' );
			wp_add_inline_style( 'wfs-front', $style );
		}

		return $instance;
	}

	public function enqueue_scripts() {
		// Register a dummy style handle. We'll use this to enqueue our dynamic styles.
		wp_register_style( 'wfs-front', false );
	}

	protected function is_supported( WP_Widget $widget ) {
		if ( in_array( $widget->id_base, $this->widgets, true ) ) {
			return true;
		}

		return false;
	}
}

new Widgets_Font_Size();

There you have it. Did you find it easy? Did you find it useful? Let us know in the comments.

Subscribe to our newsletter.

Get fresh WordPress content straight into your inbox. We hate spam more than you do.

Leave a Reply

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