WordPress Shortcodes — How to Create Custom Shortcodes for Content and Layout
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
Use a unique prefix for shortcode names.
dodatech_buttonnotbutton, which could conflict with other plugins.Return strings, never echo. Shortcodes gather output and return it. Echoing breaks the content position and can cause header errors.
Escape all output. Use
esc_html(),esc_url(), andesc_attr()on any dynamic values. Shortcodes process user input; unescaped output is an XSS vulnerability.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.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
Echoing instead of returning. The most common shortcode bug. The output appears at the top of the page or breaks the layout. Always return.
Not escaping attribute values. A user types
[button url="<a href="/programming-languages/javascript/">JavaScript</a>:alert(1)"]. Withoutesc_url(), this executes JavaScript. Always escape before output.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.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.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
- Why must shortcode callbacks return values instead of echoing them? What happens if you echo inside a shortcode?
- How does shortcode_atts() merge user attributes with defaults, and why is the third parameter (shortcode name) valuable?
- 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
Mini Project
Build a "Content Showcase" shortcode system for a portfolio site:
- Create
[portfolio count="6" category="design" orderby="date"]that displays a grid of portfolio items using WP_Query - The shortcode accepts: count (default 6), category (default empty), orderby (default 'date'), columns (default 3)
- Each grid item shows: featured image, title, excerpt (truncated to 20 words), and category links
- Add a
[portfolio_filter]shortcode that outputs category filter buttons (all, design, development, branding) - Use do_shortcode inside the filter output so buttons are actual shortcodes
- The portfolio shortcode should return a responsive grid with proper CSS classes
- 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:
- Hooks — Hook into content filters to modify shortcode output
- PHP Programming Guide — Strengthen your PHP for advanced shortcode logic
- Custom Post Types — Create CPTs that your shortcodes display
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro