Widgets & Shortcodes
Widgets and shortcodes are two of the friendliest ways WordPress lets a non-technical site
owner reuse functionality that you, the developer, build. A widget is a small
block of content or functionality dropped into a defined "widget area" — usually a sidebar or
footer — through drag-and-drop in the admin. A shortcode is a bracketed tag like
[current_year] that an editor types directly inside a post or page, and WordPress
swaps it out for dynamic PHP-generated output when the page renders. Both features exist so that
people who can't write PHP can still place dynamic content exactly where they want it.
1. Registering a Widget Area (Sidebar)
Before any widget can be placed somewhere, the theme has to declare a "widget area" for it to
live in. This is done with register_sidebar() inside functions.php, hooked
to widgets_init — the same functions.php file mentioned back in
Theme Development Basics.
<?php
function mytheme_widgets_init() {
register_sidebar([
'name' => 'Main Sidebar',
'id' => 'sidebar-main',
'description' => 'Widgets shown on blog posts and pages',
'before_widget' => '<div class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
]);
}
add_action('widgets_init', 'mytheme_widgets_init');
The id is what WordPress uses internally to remember which widgets were dropped into
this area. before_widget/after_widget and before_title/after_title
are HTML wrappers applied automatically around every widget placed here, so all widgets in the
area get consistent markup without each widget needing to repeat it.
2. Displaying Widgets in a Template
Once a sidebar is registered, a template file (like sidebar.php) has to actually
print whatever widgets the site owner dragged into it. That's the job of dynamic_sidebar() —
it takes the same id used in register_sidebar() and outputs every widget
assigned to that area, in the order the admin arranged them.
<?php if (is_active_sidebar('sidebar-main')) : ?>
<div class="widget-area">
<?php dynamic_sidebar('sidebar-main'); ?>
</div>
<?php endif; ?>
dynamic_sidebar() in an is_active_sidebar() check — if the site owner hasn't added any widgets yet, this avoids printing an empty, oddly-styled wrapper div.3. Building a Custom Widget with WP_Widget
Sometimes the built-in widgets (recent posts, search box, text) aren't enough, and you need
your own — for example, a "Latest Testimonial" widget. Custom widgets are built by extending the
abstract WP_Widget class and implementing three key methods:
__construct()— registers the widget's ID and name so it shows up in the admin's widget listwidget()— outputs the actual HTML shown on the front endform()— outputs the settings form shown in the admin when the widget is configuredupdate()— sanitizes and saves the values submitted from that settings form
<?php
class Testimonial_Widget extends WP_Widget {
function __construct() {
parent::__construct(
'testimonial_widget',
'Testimonial Widget',
['description' => 'Shows a single testimonial']
);
}
// Front-end output
function widget($args, $instance) {
echo $args['before_widget'];
if (!empty($instance['title'])) {
echo $args['before_title'] . esc_html($instance['title']) . $args['after_title'];
}
echo '<p>' . esc_html($instance['text'] ?? '') . '</p>';
echo $args['after_widget'];
}
// Admin settings form
function form($instance) {
$title = $instance['title'] ?? '';
$text = $instance['text'] ?? '';
?>
<p><label>Title<input class="widefat" name="<?php echo $this->get_field_name('title'); ?>" value="<?php echo esc_attr($title); ?>"></label></p>
<p><label>Text<textarea class="widefat" name="<?php echo $this->get_field_name('text'); ?>"><?php echo esc_textarea($text); ?></textarea></label></p>
<?php
}
// Save sanitized values
function update($new_instance, $old_instance) {
$instance = [];
$instance['title'] = sanitize_text_field($new_instance['title']);
$instance['text'] = sanitize_textarea_field($new_instance['text']);
return $instance;
}
}
function register_testimonial_widget() {
register_widget('Testimonial_Widget');
}
add_action('widgets_init', 'register_testimonial_widget');
widget() escapes with esc_html() on the way out, while update() sanitizes with sanitize_text_field() on the way in — this "sanitize in, escape out" pattern is covered fully in Security Best Practices.4. Shortcodes: Dynamic Content Inside Post Content
A widget only ever appears in a widget area like a sidebar. A shortcode solves a
different problem: letting an editor drop dynamic, PHP-generated output inline, wherever
they're typing inside the post/page editor — by simply writing something like
[current_year] in a bracket tag. WordPress scans post content for these tags before
rendering and replaces each one with whatever your callback function returns.
5. Registering a Simple Shortcode
Shortcodes are registered with add_shortcode(), which takes a tag name and a callback function. The callback must return its output — never echo it — because WordPress inserts the returned string directly into the content stream.
<?php
function current_year_shortcode() {
return date('Y');
}
add_shortcode('current_year', 'current_year_shortcode');
// An editor types [current_year] in a post,
// and it renders as the current year, e.g. 2026
6. Shortcodes with Attributes
Real-world shortcodes usually need configuration — for example a button shortcode where the
editor supplies the label and destination URL:
[button text="Click Me" url="/contact"]. Attributes arrive in the callback's first
parameter as a raw associative array, so shortcode_atts() is used to merge them
against a set of safe defaults — this way the shortcode never breaks if an editor omits an
attribute.
<?php
function button_shortcode($atts) {
$atts = shortcode_atts([
'text' => 'Click Here',
'url' => '#',
], $atts, 'button');
return sprintf(
'<a class="btn" href="%s">%s</a>',
esc_url($atts['url']),
esc_html($atts['text'])
);
}
add_shortcode('button', 'button_shortcode');
// [button text="Contact Us" url="/contact"]
// renders as <a class="btn" href="/contact">Contact Us</a>
shortcode_atts() (the shortcode tag name) enables a shortcode_atts_{tag} filter, letting other plugins adjust defaults for your shortcode — another example of the hook system from Hooks: Actions & Filters at work.7. Widget vs. Shortcode: Which One to Use
| Use a Widget when... | Use a Shortcode when... |
|---|---|
| The content belongs in a sidebar, footer, or other dedicated widget area | The content needs to appear inline, mixed in with the post/page body text |
| A site owner should be able to drag-and-drop it into place visually | An editor should be able to type a tag directly while writing content |
| The same block should appear consistently across many pages (e.g. a "Recent Posts" box) | The output should vary per-post depending on where and how it's placed |