Create Shortcodes in WordPress (With Practical Examples)
Shortcodes let you insert dynamic content into posts, pages, and widgets using a simple syntax like [year] or [button url="..."]Text[/button]. In this guide you’ll learn how to build secure, reusable shortcodes with practical examples you can paste into functions.php or a small utility plugin.
How Shortcodes Work
- Self-closing shortcodes have no content:
[year] - Enclosing shortcodes wrap content:
[button]Buy now[/button] - Shortcodes must return a string (do not echo).
- Always sanitize user attributes and escape output.
Example 1: Current Year (Self-Closing)
Simple, safe, and useful for footers or copyright lines.
<?php
// [year] → 2025
add_shortcode( 'year', function () {
return esc_html( gmdate( 'Y' ) );
} );
Example 2: Button (Attributes + Enclosing Content)
Supports attributes with defaults via shortcode_atts() and wraps content.
<?php
// [button url="https://example.com" target="_blank" rel="nofollow"]Read more[/button]
add_shortcode( 'button', function ( $atts, $content = null ) {
$atts = shortcode_atts( array(
'url' => '#',
'target' => '',
'rel' => '',
'class' => 'wpt-btn',
), $atts, 'button' );
$url = esc_url( $atts['url'] );
$target = $atts['target'] ? ' target="' . esc_attr( $atts['target'] ) . '"' : '';
$rel = $atts['rel'] ? ' rel="' . esc_attr( $atts['rel'] ) . '"' : '';
$class = ' class="' . esc_attr( $atts['class'] ) . '"';
$label = $content !== null ? $content : '';
$label = wp_kses_post( $label ); // allow basic inline HTML if needed
return '<a href="' . $url . '"' . $target . $rel . $class . '>' . $label . '</a>';
} );
Example 3: Conditional Greeting (Current User)
Shows different output depending on login state.
<?php
// [greet] → "Hello, Jane" if logged in, else "Hello, Guest"
add_shortcode( 'greet', function () {
if ( is_user_logged_in() ) {
$user = wp_get_current_user();
return 'Hello, ' . esc_html( $user->display_name );
}
return 'Hello, Guest';
} );
Example 4: Query Posts with Caching (Performance)
Heavy queries should be cached with transients. This lists latest 5 posts as links.
<?php
// [latest_posts count="5" cat="news"]
add_shortcode( 'latest_posts', function ( $atts ) {
$atts = shortcode_atts( array(
'count' => 5,
'cat' => '', // category slug (optional)
), $atts, 'latest_posts' );
$count = max( 1, min( 20, (int) $atts['count'] ) ); // clamp 1..20
$cat = sanitize_title( $atts['cat'] );
$cache_key = 'wpt_latest_posts_' . md5( $count . '|' . $cat );
$html = get_transient( $cache_key );
if ( false === $html ) {
$args = array(
'posts_per_page' => $count,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
);
if ( $cat ) {
$args['category_name'] = $cat;
}
$q = new WP_Query( $args );
if ( ! $q->have_posts() ) {
return ''; // nothing to show
}
$items = '';
while ( $q->have_posts() ) {
$q->the_post();
$items .= '<li><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a></li>';
}
wp_reset_postdata();
$html = '<ul class="wpt-latest-posts">' . $items . '</ul>';
set_transient( $cache_key, $html, HOUR_IN_SECONDS );
}
return $html;
} );
Example 5: Enclosing Content Processor (TOC Wrapper)
Transforms enclosed headings into a simple Table of Contents list (demo-safe).
<?php
// [toc]<h2>A</h2>...[/toc] → extracts <h2>..</h2> into a list
add_shortcode( 'toc', function ( $atts, $content = null ) {
if ( empty( $content ) ) return '';
// Minimal extraction for demo (use a robust parser in production)
preg_match_all( '/<h2[^>]*>(.*?)<\\/h2>/i', $content, $matches );
if ( empty( $matches[1] ) ) {
return wp_kses_post( $content );
}
$list = '';
foreach ( $matches[1] as $i => $text ) {
$anchor = 'sec-' . ($i+1);
$list .= '<li><a href="#' . esc_attr( $anchor ) . '">' . wp_kses_post( $text ) . '</a></li>';
// inject IDs into headings
$content = preg_replace('/<h2([^>]*)>' . preg_quote($text, '/') . '<\\/h2>/i', '<h2 id="' . $anchor . '"$1>' . $text . '</h2>', $content, 1);
}
$toc = '<nav class="wpt-toc"><strong>Contents</strong><ol>' . $list . '</ol></nav>';
return $toc . wp_kses_post( $content );
} );
Example 6: Shortcode That Loads a Template Part
Keep markup in a separate file and reuse it anywhere with a shortcode.
<?php
// /your-child-theme/template-parts/price-table.php → expects $atts
// [price_table plan="pro" price="$29"]
add_shortcode( 'price_table', function ( $atts ) {
$atts = shortcode_atts( array(
'plan' => 'basic',
'price' => '$0',
), $atts, 'price_table' );
// Output buffering to capture template output
ob_start();
$safe_atts = array_map( 'sanitize_text_field', $atts );
// Make variables available in template scope
$plan = $safe_atts['plan'];
$price = $safe_atts['price'];
$file = get_stylesheet_directory() . '/template-parts/price-table.php';
if ( file_exists( $file ) ) {
include $file;
} else {
echo '<div class="price-table"><strong>' . esc_html( $plan ) . '</strong> – ' . esc_html( $price ) . '</div>';
}
return ob_get_clean();
} );
Security & Best Practices
- Sanitize attributes:
sanitize_text_field(),esc_url_raw()etc. - Escape output:
esc_html(),esc_url(),wp_kses_post(). - Return, don’t echo: Shortcodes must return a string.
- Scope & naming: Prefix handles (e.g.,
wpt_) to avoid conflicts. - Cache heavy work: Use transients for queries/remote calls.
- Don’t run untrusted content: Never eval or include paths from user input.
Where Can Shortcodes Be Used?
- Classic Editor / Blocks: Use the “Shortcode” block in Gutenberg.
- Widgets: Enable shortcodes in text widgets if needed:
<?php
// Allow shortcodes in widget text (classic widgets)
add_filter( 'widget_text', 'do_shortcode' );
Disable Shortcodes in Excerpts (Optional)
If shortcodes appear raw in excerpts, strip them:
<?php
// Remove shortcodes from auto-generated excerpts
add_filter( 'the_excerpt', function ( $text ) {
return strip_shortcodes( $text );
} );
Debugging Tips
- If output is empty, check that your callback returns a string.
- Verify your shortcode tag is unique and not used by a plugin or theme.
- Temporarily
var_dump()orerror_log()inside the callback.
Conclusion
Shortcodes are a lightweight way to reuse dynamic pieces across your site. Start with simple patterns (year, buttons), then move to cached queries and template-powered components. Keep them secure, cached, and neatly namespaced, and they’ll remain a reliable tool alongside modern blocks.
🎨 Want to learn more? Visit our WordPress Customization Hub for tips and advanced techniques.