
How to Create a WordPress Shortcode?
A shortcode is a small bracketed tag, something like [button text="Buy Now" url="/shop"], that WordPress swaps out for dynamic HTML wherever it appears in your content. They're what makes it possible for an editor to drop a styled button, an embedded form, or a pricing table into a post without knowing any PHP or HTML themselves, and without a developer needing to hand-edit that specific page.
Once you understand the pattern, building your own shortcode takes only a few lines of code, and it's one of the most reusable tools in a WordPress developer's toolkit, since the same shortcode works identically whether it's placed in a post, a page, a widget, or even inside a template file. This guide walks through the full Shortcode API: a basic shortcode, one that accepts attributes, one that wraps content, and how to load its assets only where it's actually used.
How the Shortcode API Works
Every shortcode is built from two pieces:
add_shortcode( $tag, $callback )registers the tag (what goes inside the brackets) and connects it to a PHP function.- The callback function runs whenever WordPress encounters that tag in content, and whatever it
returns replaces the shortcode in the final output.
The one rule that trips up almost everyone building their first shortcode: the callback must return its output, never echo it. Shortcodes are processed while WordPress is still assembling content in memory, well before anything is sent to the browser, so anything you echo ends up printed in the wrong place on the page, usually above your site's header.
Step 1: Build a Basic Shortcode
Add this to your theme's functions.php, or, better, a must-use plugin so it keeps working if you ever switch themes:
add_shortcode( 'current_year', function () {
return gmdate( 'Y' );
} );
Now typing [current_year] into any post or page's content prints the current year, and it will keep updating automatically every year without anyone needing to edit that page again. It's a tiny example, but it demonstrates the whole pattern: register a tag, return a string, done.
Step 2: Add Attributes
Most useful shortcodes need to accept options, the way [button text="Buy Now" url="/shop"] does. WordPress passes anything written inside the tag to your callback as an array, and shortcode_atts() is what merges that array with sensible defaults:
add_shortcode( 'button', function ( $atts ) {
$atts = shortcode_atts( [
'text' => 'Click Here',
'url' => '#',
'color' => 'primary',
], $atts, 'button' );
return sprintf(
'<a href="%s" class="tw-button tw-button--%s">%s</a>',
esc_url( $atts['url'] ),
esc_attr( $atts['color'] ),
esc_html( $atts['text'] )
);
} );
A few things worth understanding in this example:
shortcode_atts()takes three arguments: your defaults, the attributes actually passed in, and the shortcode's tag name (passed for a filter hook other developers can use to adjust your defaults). Any attribute the user doesn't specify falls back to your default automatically.esc_url(),esc_attr(), andesc_html()are not optional here. Since shortcode attributes ultimately come from whatever an editor typed into the post editor, always escape them on the way out, exactly as you would any other user-supplied data.- Now
[button text="Buy Now" url="/shop" color="primary"]renders a fully-formed, safely-escaped link, and[button]alone still works, falling back to every default.
Step 3: Handle Enclosed Content
Some shortcodes need to wrap other content, rather than just accepting attributes, the way [highlight]this text[/highlight] wraps "this text" between an opening and closing tag. That content arrives as a second parameter to your callback:
add_shortcode( 'highlight', function ( $atts, $content = null ) {
$atts = shortcode_atts( [
'color' => '#fff3cd',
], $atts, 'highlight' );
if ( is_null( $content ) ) {
return '';
}
return sprintf(
'<mark style="background-color:%s;">%s</mark>',
esc_attr( $atts['color'] ),
do_shortcode( $content )
);
} );
Two details matter here:
$content = nullas the default lets you tell the difference between a self-closing shortcode ([highlight], with no closing tag) and one that's missing its expected content, so you can return early instead of outputting a broken<mark>tag with nothing inside it.- Wrapping
$contentindo_shortcode()before returning it processes any nested shortcodes inside the enclosed content, so[highlight][current_year][/highlight]correctly renders the year inside the highlight, rather than printing the literal text[current_year].
Step 4: Only Load CSS/JS Where the Shortcode Is Actually Used
If your shortcode needs its own stylesheet or script, loading it on every single page (just in case the shortcode shows up somewhere) wastes a request on every page that doesn't use it. has_shortcode() lets you check the current post's content before deciding to enqueue anything:
add_action( 'wp_enqueue_scripts', function () {
if ( is_singular() && has_shortcode( get_post()->post_content, 'button' ) ) {
wp_enqueue_style(
'tw-button-style',
get_stylesheet_directory_uri() . '/css/button-shortcode.css',
[],
'1.0.0'
);
}
} );
This is worth doing for any shortcode with meaningful CSS or JS attached to it, since it's the difference between a handful of assets loading site-wide forever versus only loading on the handful of pages that actually need them.
Step 5: Use a Shortcode Inside a Template File
Shortcodes aren't limited to the post content editor. If you want to run one directly from a template file (for example, to always show a "Related Products" shortcode at the bottom of every single product page), call do_shortcode() directly:
<?php echo do_shortcode( '[related_products count="4"]' ); ?>
This is the same function used internally by Step 3 to process nested shortcodes, and it's the standard way to invoke a shortcode from PHP rather than from post content.
Making a Shortcode Available in Widgets and the Block Editor
By default, shortcodes only run inside post and page content, but WordPress core adds the same processing to text widgets automatically via the widget_text_content filter (as of WordPress 4.9), so [current_year] typed into a Custom HTML or text widget already works without any extra code on your end.
In the block editor, the same tag works by adding it inside a Shortcode block, which exists specifically so a bracketed tag typed there gets processed the same way it would in classic post content.
If you've built a shortcode that isn't rendering somewhere you expect (a custom template area, for example, that doesn't normally run content through the_content filter), that's exactly when you'd reach for do_shortcode() directly, as shown in Step 5, rather than assuming every context processes shortcodes automatically.
Security Considerations for Shortcodes
A shortcode that outputs unescaped data is a real cross-site scripting (XSS) risk, since its attributes ultimately come from whatever gets typed into the editor, which may not always be you:
- Always escape output. Use
esc_html()for plain text,esc_attr()for anything placed inside an HTML attribute, andesc_url()for anything used as a linkhreforsrc, exactly as shown in the examples above. - Never trust
$contentblindly. If your shortcode wraps content, decide deliberately whether to allow HTML inside it. If not, strip it withwp_strip_all_tags()before use. - Validate attribute values, not just their type. If an attribute should be one of a fixed set of options (like the
colorattribute above), check it against an allow-list rather than outputting whatever string was passed in:
$allowed_colors = [ 'primary', 'secondary', 'danger' ];
if ( ! in_array( $atts['color'], $allowed_colors, true ) ) {
$atts['color'] = 'primary';
}
Frequently Asked Questions (FAQ) About WordPress Shortcodes
The most common cause is forgetting to return your output instead of echoing it. WordPress replaces the shortcode tag with whatever your callback function returns; anything echoed inside the callback prints elsewhere on the page instead, often near the very top, and the shortcode tag itself is simply replaced with an empty string.
Yes, the callback is a normal PHP function, so it can query the database, run a loop, or do anything else a template file can do. Just make sure the final result is built into a single string (often with output buffering via ob_start() and ob_get_clean() for more complex markup) and returned, not echoed.
The second add_shortcode() call overwrites the first, since only one callback can be registered per tag at a time. This is a common conflict between plugins or between a plugin and a theme; if a shortcode suddenly behaves differently after installing a new plugin, check whether it registered a shortcode using the same tag name.
Yes. The Shortcode API is a core WordPress feature independent of your theme or the block editor, and the dedicated Shortcode block exists specifically to keep this working in the block editor. Shortcodes registered in a must-use plugin (rather than a classic theme's functions.php) will also survive a theme switch, since they aren't tied to the theme at all.
For anything primarily visual with live editing needs (drag-and-drop styling, a real-time preview in the editor), a custom block is generally the better long-term investment. Shortcodes remain the simpler, faster option for smaller utilities, content that needs to work inside classic widgets, or when you're supporting older content and plugins that already rely on bracket syntax.
Yes, wrap the value in quotes when using the shortcode: [button text="Buy it now"]. WordPress's shortcode parser correctly handles quoted attribute values containing spaces; the issue only comes up if the quotes are left off entirely.
Conclusion
The Shortcode API is one of the oldest parts of WordPress, and it's stayed relevant because the pattern is so simple: register a tag, return a string, and let editors reuse it anywhere without touching code. Start with shortcode_atts() for anything that needs configurable options, remember that enclosed content arrives as a second parameter you should run through do_shortcode(), and treat every attribute as untrusted input that needs escaping on the way out, the same way you would any other user-supplied data.
Once you're comfortable with the basic pattern, it scales well beyond simple buttons and highlights: pricing tables, embedded forms, and dynamic content blocks all follow exactly the same add_shortcode() structure shown here.
Here are a few additional resources if you want to go deeper:
- WordPress Developer Reference: add_shortcode() — the full function reference and parameter details.
- WordPress Developer Reference: shortcode_atts() — how attribute defaults and filtering work.
- WordPress Plugin Handbook: Shortcodes — the official guide covering advanced patterns like nested and self-closing shortcodes.


