Create Shortcodes in WordPress (With Practical Examples)

October 5, 2025
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() or error_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.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.