Skip to content

WordPress Shortcodes — How to Create Custom Shortcodes for Content and Layout

DodaTech Updated 2026-06-27 12 min read

In this tutorial, you'll learn to create WordPress shortcodes using add_shortcode() — handling attributes with shortcode_atts(), enclosing content, nested shortcodes, and practical examples for both themes and plugins.

What You'll Learn

  • What shortcodes are and how WordPress replaces them with dynamic output
  • How add_shortcode() registers a shortcode and maps it to a callback
  • How shortcode callbacks receive attributes, enclosed content, and the tag name
  • How shortcode_atts() handles default attribute values
  • The difference between self-closing and enclosing shortcodes
  • Why shortcodes must return strings, never echo output
  • How to enable shortcodes in widgets and custom fields
  • How to remove and override existing shortcodes
  • How nested shortcodes work and their limitations
  • Practical shortcode examples: buttons, highlights, current year, tooltips

Why It Matters

Imagine a content editor who needs to add a styled button inside a blog post. Without shortcodes, they need to write HTML: <a href="/contact" class="btn btn-primary">Contact Us</a>. They might get the class wrong, forget the closing tag, or create inconsistent styling. With a shortcode, they type [button url="/contact" color="blue"]Contact Us[/button] — clean, consistent, and impossible to break. Shortcodes give non-technical editors access to complex functionality through simple, memorable syntax. They are a bridge between developers who write PHP and content creators who write text.

Real-World Use

A magazine site needs to embed download buttons, info boxes, toggle sections, and author credit blocks throughout articles. The editor should not need to know HTML or remember CSS classes. The developer creates shortcodes: [download url="..."], [infobox]content[/infobox], [toggle title="Click to expand"]hidden content[/toggle], [author]. The editor types these in the Gutenberg Classic block or standard editor. Each shortcode renders consistent, styled output. The site maintains visual consistency across hundreds of articles.

Learning Path

flowchart LR
    A[Hooks] --> B["Shortcodes
You are here"]:::current B --> C[REST API] C --> D[Localization] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

What Are Shortcodes?

A shortcode is a WordPress-specific markup syntax: square brackets containing a tag name, optional attributes, and optional enclosed content. When WordPress processes content, it finds shortcodes, replaces them with the output of the registered callback, and returns the modified content.

[my_shortcode]             → self-closing
[my_shortcode]content[/my_shortcode]  → enclosing
[my_shortcode key="value"]            → with attributes

Think of shortcodes as macros in a word processor. You type a shorthand code, and WordPress expands it into full HTML. The user never sees the complex markup — just the clean shortcode syntax in the editor and the polished result on the front end.

The add_shortcode() Function

add_shortcode( string $tag, callable $callback );
  • $tag — the shortcode name inside square brackets: [my_shortcode]
  • $callback — the function that generates output when the shortcode is found

Place shortcode registrations in your theme's functions.php or within a plugin file, typically on the init hook or directly:

function dodatech_button_shortcode( $atts, $content = null, $tag = '' ) {
    // return the HTML output
}
add_shortcode( 'button', 'dodatech_button_shortcode' );

Shortcode Callback Parameters

Every shortcode callback receives three parameters:

Parameter Description
$atts Array of attribute key-value pairs from the shortcode tag
$content Enclosed content (null for self-closing shortcodes)
$tag The shortcode tag name (useful when one callback handles multiple tags)
function dodatech_example_shortcode( $atts, $content, $tag ) {
    // $atts    = array( 'url' => 'https://example.com', 'color' => 'blue' )
    // $content = 'Click Here' (if enclosing)
    // $tag     = 'button'
}

Shortcode Attributes with shortcode_atts()

Attributes let users customize shortcode behavior. shortcode_atts() merges user-provided attributes with defaults:

function dodatech_button_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts(
        array(
            'url'    => '#',
            'color'  => 'blue',
            'target' => '_self',
            'size'   => 'medium',
        ),
        $atts,
        'button'
    );

    $class = 'btn btn-' . esc_attr( $atts['color'] ) . ' btn-' . esc_attr( $atts['size'] );

    return '<a href="' . esc_url( $atts['url'] ) . '"'
         . ' target="' . esc_attr( $atts['target'] ) . '"'
         . ' class="' . $class . '">'
         . esc_html( $content )
         . '</a>';
}
add_shortcode( 'button', 'dodatech_button_shortcode' );

Usage in the editor:

[button url="https://dodatech.com" color="red" size="large"]Visit DodaTech[/button]

The third parameter of shortcode_atts() is the shortcode name. It enables other developers to filter default attributes using the shortcode_atts_button filter.

Self-Closing vs Enclosing Shortcodes

Self-Closing

[year]
[google_map address="123 Main St" zoom="15"]
[download id="42"]

No content between opening and closing tags. The callback receives $content = null.

function dodatech_year_shortcode() {
    return date( 'Y' );
}
add_shortcode( 'year', 'dodatech_year_shortcode' );

Enclosing

[highlight color="yellow"]Important text[/highlight]
[button url="/download"]Download Now[/button]
[tooltip text="Click for help"]Hover over me[/tooltip]

Content appears between the opening and closing tags. The callback receives the content as $content.

function dodatech_highlight_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts(
        array( 'color' => 'yellow' ),
        $atts,
        'highlight'
    );
    return '<span style="background-color: ' . esc_attr( $atts['color'] ) . ';">'
         . esc_html( $content )
         . '</span>';
}
add_shortcode( 'highlight', 'dodatech_highlight_shortcode' );

Shortcodes Must Return, Never Echo

This is the most important rule of shortcodes: your callback must return the output string, not echo it.

// WRONG — echoes output, breaks content positioning
function dodatech_bad_shortcode() {
    echo '<p>This breaks the page</p>';
}

// CORRECT — returns the string
function dodatech_good_shortcode() {
    return '<p>This works correctly</p>';
}

Why? Shortcode processing happens when WordPress filters the content. If you echo, the output appears before the content starts, breaking the layout. WordPress captures the return value and inserts it exactly where the shortcode appears.

Shortcodes in Widgets

By default, WordPress does not Process shortcodes in text widgets. Enable them with a filter:

add_filter( 'widget_text', 'do_shortcode' );

This changes the widget_text filter to run do_shortcode() on the widget content, processing any shortcodes the editor typed.

Shortcodes in Custom Fields

If you want shortcodes to work in custom field values, call do_shortcode() when displaying:

echo do_shortcode( get_post_meta( get_the_ID(), 'custom_field', true ) );

Without this, the shortcode text displays literally instead of being replaced.

Removing Shortcodes

remove_shortcode( 'gallery' );

This deregisters a shortcode entirely. If you want to override an existing shortcode, register yours after removing the original:

remove_shortcode( 'gallery' );
add_shortcode( 'gallery', 'dodatech_custom_gallery_shortcode' );

Nested Shortcodes

WordPress handles nested shortcodes poorly by default. This does not work as expected:

[columns]
  [column]Left content[/column]
  [column]Right content[/column]
[/columns]

WordPress processes the innermost shortcodes first, but the outer shortcode receives already-processed content. For nested shortcodes, call do_shortcode() inside your callback:

function dodatech_columns_shortcode( $atts, $content = null ) {
    return '<div class="columns">'
         . do_shortcode( $content )
         . '</div>';
}
add_shortcode( 'columns', 'dodatech_columns_shortcode' );

function dodatech_column_shortcode( $atts, $content = null ) {
    return '<div class="column">'
         . do_shortcode( $content )
         . '</div>';
}
add_shortcode( 'column', 'dodatech_column_shortcode' );

Usage:

[columns]
  [column]Left content[/column]
  [column]Right content[/column]
[/columns]

Practical Shortcode Examples

Current Year Shortcode

function dodatech_year_shortcode() {
    return date( 'Y' );
}
add_shortcode( 'year', 'dodatech_year_shortcode' );

Tooltip Shortcode

function dodatech_tooltip_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts(
        array( 'text' => 'Tooltip' ),
        $atts,
        'tooltip'
    );
    return '<span class="tooltip" data-tooltip="' . esc_attr( $atts['text'] ) . '">'
         . esc_html( $content )
         . '</span>';
}
add_shortcode( 'tooltip', 'dodatech_tooltip_shortcode' );

Download Button Shortcode

function dodatech_download_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts(
        array(
            'url'  => '#',
            'size' => '16',
        ),
        $atts,
        'download'
    );

    $content = $content ?: 'Download';

    return '<a href="' . esc_url( $atts['url'] ) . '"'
         . ' class="btn-download"'
         . ' download>'
         . esc_html( $content )
         . ' (' . esc_html( $atts['size'] ) . ' MB)'
         . '</a>';
}
add_shortcode( 'download', 'dodatech_download_shortcode' );

Info Box Shortcode

function dodatech_infobox_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts(
        array(
            'type'  => 'info', // info, warning, success, error
            'title' => '',
        ),
        $atts,
        'infobox'
    );

    $output = '<div class="infobox infobox-' . esc_attr( $atts['type'] ) . '">';
    if ( $atts['title'] ) {
        $output .= '<h4 class="infobox-title">' . esc_html( $atts['title'] ) . '</h4>';
    }
    $output .= '<div class="infobox-content">' . do_shortcode( $content ) . '</div>';
    $output .= '</div>';

    return $output;
}
add_shortcode( 'infobox', 'dodatech_infobox_shortcode' );

Toggle / Accordion Shortcode

function dodatech_toggle_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts(
        array( 'title' => 'Toggle' ),
        $atts,
        'toggle'
    );

    $id = 'toggle-' . wp_rand( 1000, 9999 );

    return '<div class="toggle">'
         . '<button class="toggle-trigger" aria-expanded="false"'
         . ' aria-controls="' . $id . '">'
         . esc_html( $atts['title'] )
         . '</button>'
         . '<div class="toggle-content" id="' . $id . '" hidden>'
         . do_shortcode( $content )
         . '</div>'
         . '</div>';
}
add_shortcode( 'toggle', 'dodatech_toggle_shortcode' );

Shortcode Output in PHP Context

You can also render shortcodes directly in PHP template files:

<?php echo do_shortcode( '[button url="/contact" color="green"]Contact[/button]' ); ?>

This is useful when you want to reuse shortcode output in widget areas or custom template parts without asking editors to type the shortcode.

One Callback for Multiple Shortcodes

Register multiple shortcode tags to the same callback and use the $tag parameter to differentiate:

function dodatech_social_shortcode( $atts, $content = null, $tag = '' ) {
    $atts = shortcode_atts(
        array( 'url' => '#' ),
        $atts,
        $tag
    );

    $icons = array(
        'twitter'   => '/images/twitter.svg',
        'facebook'  => '/images/facebook.svg',
        'linkedin'  => '/images/linkedin.svg',
        'youtube'   => '/images/youtube.svg',
    );

    if ( ! isset( $icons[ $tag ] ) ) {
        return '';
    }

    return '<a href="' . esc_url( $atts['url'] ) . '" class="social-icon">'
         . '<img src="' . esc_url( $icons[ $tag ] ) . '"'
         . ' alt="' . esc_attr( ucfirst( $tag ) ) . '">'
         . '</a>';
}

add_shortcode( 'twitter',  'dodatech_social_shortcode' );
add_shortcode( 'facebook', 'dodatech_social_shortcode' );
add_shortcode( 'linkedin', 'dodatech_social_shortcode' );
add_shortcode( 'youtube',  'dodatech_social_shortcode' );

Usage:

[twitter url="https://twitter.com/dodatech"]
[linkedin url="https://linkedin.com/company/dodatech"]

Shortcode Best Practices

  1. Use a unique prefix for shortcode names. dodatech_button not button, which could conflict with other plugins.

  2. Return strings, never echo. Shortcodes gather output and return it. Echoing breaks the content position and can cause header errors.

  3. Escape all output. Use esc_html(), esc_url(), and esc_attr() on any dynamic values. Shortcodes process user input; unescaped output is an XSS vulnerability.

  4. Keep shortcode output simple. Shortcodes generate HTML. If your shortcode generates 50 lines of HTML, consider whether a template partial with do_shortcode() might be cleaner.

  5. Document your shortcodes. Write a comment or readme listing every shortcode, its attributes, default values, and example usage. Other developers (and future you) will thank you.

Common Mistakes

  1. Echoing instead of returning. The most common shortcode bug. The output appears at the top of the page or breaks the layout. Always return.

  2. Not escaping attribute values. A user types [button url="<a href="/programming-languages/javascript/">JavaScript</a>:alert(1)"]. Without esc_url(), this executes JavaScript. Always escape before output.

  3. Forgetting do_shortcode for nested content. When an enclosing shortcode wraps other shortcodes, the inner shortcodes are not processed unless you call do_shortcode($content) inside the outer callback.

  4. Naming shortcodes with hyphens in attributes. [my-shortcode attr="value"] is fine. But naming a shortcode with a hyphen like [my-shortcode] works because WordPress treats the hyphen as part of the name. However, some plugins use hyphenated names, so check for conflicts.

  5. Shortcodes inside shortcodes not working in widgets. Widgets do not process shortcodes by default. Add add_filter('widget_text', 'do_shortcode') to enable them. Users often expect this to work automatically.

Practice Questions

  1. Why must shortcode callbacks return values instead of echoing them? What happens if you echo inside a shortcode?
  2. How does shortcode_atts() merge user attributes with defaults, and why is the third parameter (shortcode name) valuable?
  3. What is the difference between [gallery ids="1,2,3"] and [gallery]1,2,3[/gallery] in terms of how the callback receives the data?

Challenge: Create a set of shortcodes for a pricing table:

  • [pricing_table] — outer wrapper that calls do_shortcode on content
  • [pricing_column title="Basic" price="$9" featured="true"] — column with attributes for title, price, currency, interval, and featured status
  • [pricing_feature]text[/pricing_feature] — individual feature inside a column
  • Output a three-column pricing table with proper HTML structure and responsive CSS classes

FAQ

### Can shortcodes include PHP logic or database queries?

Yes. Shortcode callbacks are regular PHP functions. They can query the database, call WordPress functions, include files, and execute any logic. However, avoid heavy queries inside shortcodes since they run every time the content is displayed and may be called multiple times per page.

How do I pass complex data like arrays to a shortcode?

Shortcode attributes are always strings. For complex data, pass comma-separated values and explode them inside the callback: ids="1,2,3" becomes array(1, 2, 3) after explode(',', $atts['ids']). For large datasets, use a separate parameter like category="featured" and query the data inside the callback.

Do shortcodes work in Gutenberg?

Yes, but the shortcode output appears as a gray placeholder in the editor. Gutenberg has a dedicated Shortcode block that renders the shortcode in the editor using an iframe. Users cannot see the full styled output until previewing or publishing.

Can I create a shortcode that modifies the query or global state?

You can, but it is risky. Shortcodes run during content rendering, after the main query. Modifying globals inside a shortcode can break other shortcodes, widgets, and the main loop. If you need to modify state, use hooks instead.

What is the maximum number of shortcodes per page?

Technically unlimited, but each shortcode adds processing time. A page with 50 shortcodes that each query the database will be slow. Cache shortcode output where possible using WordPress transients.

Mini Project

Build a "Content Showcase" shortcode system for a portfolio site:

  1. Create [portfolio count="6" category="design" orderby="date"] that displays a grid of portfolio items using WP_Query
  2. The shortcode accepts: count (default 6), category (default empty), orderby (default 'date'), columns (default 3)
  3. Each grid item shows: featured image, title, excerpt (truncated to 20 words), and category links
  4. Add a [portfolio_filter] shortcode that outputs category filter buttons (all, design, development, branding)
  5. Use do_shortcode inside the filter output so buttons are actual shortcodes
  6. The portfolio shortcode should return a responsive grid with proper CSS classes
  7. Document all shortcodes with attributes, defaults, and example usage

What's Next

Now that you can create reusable content components, learn to expose your data through the REST API:

Continue to Lesson 43: REST API — Read, create, and customize WordPress REST API endpoints.

Related lessons:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro