Shortcodes are the mechanism where writing [myform] in post content calls a feature. They suit “letting editors place a fixed component without writing HTML.”
Building one takes two lines.
function my_year_shortcode() {
return esc_html(date('Y'));
}
add_shortcode('year', 'my_year_shortcode');Now writing [year] in an article displays 2026.
Receiving attributes
Being able to change appearance or content through attributes, as in [box color="red"], is where shortcodes get genuinely useful.
function my_badge_shortcode($atts) {
$a = shortcode_atts([
'label' => 'NEW',
'color' => 'pink',
], $atts, 'badge');
return sprintf(
'<span class="badge badge--%s">%s</span>',
esc_attr($a['color']),
esc_html($a['label'])
);
}
add_shortcode('badge', 'my_badge_shortcode');In the article you write it like this.
[badge] → NEW (pink)
[badge label="Limited" color="blue"] → Limited (blue)shortcode_atts() is the function that sets defaults and overrides only the attributes that were given. Running things through it prevents Undefined index errors when an attribute is missing.
The enclosing form (with an opening and closing tag)
function my_note_shortcode($atts, $content = '') {
$a = shortcode_atts(['title' => 'Point'], $atts, 'note');
return sprintf(
'<div class="note"><p class="note__title">%s</p>%s</div>',
esc_html($a['title']),
wp_kses_post(do_shortcode($content)) // process the inner HTML and shortcodes too
);
}
add_shortcode('note', 'my_note_shortcode');[note title="Careful"]
You can write <strong>text</strong> here.
[/note]The enclosed content arrives in the second argument, $content. Passing it through do_shortcode($content) makes nested shortcodes work. When you allow HTML inside, wp_kses_post() strips out dangerous tags.
When the HTML is long, capture it with ob_start
When you want to write HTML the way you would in a template, output buffering keeps it readable.
function my_products_shortcode($atts) {
$a = shortcode_atts(['count' => 3], $atts, 'products');
$query = new WP_Query([
'post_type' => 'shohin',
'posts_per_page' => (int) $a['count'],
]);
ob_start(); // start collecting output here
?>
<?php if ($query->have_posts()) : ?>
<ul class="products">
<?php while ($query->have_posts()) : $query->the_post(); ?>
<li>
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail('medium'); ?>
<span><?php the_title(); ?></span>
</a>
</li>
<?php endwhile; ?>
</ul>
<?php endif; ?>
<?php
wp_reset_postdata();
return ob_get_clean(); // return the collected output as a string
}
add_shortcode('products', 'my_products_shortcode');Write [products count="6"] in an article and the product list appears. Editors can place a list without touching PHP—that’s the main use for shortcodes.
Using one inside a template
<?php echo do_shortcode('[products count="4"]'); ?>Use this when you want to embed a shortcode a plugin provides (a contact form, say) into a template. For your own features, calling the function directly rather than going through a shortcode is simpler.
When it doesn’t work in widgets or excerpts
- Excerpts—shortcodes aren’t processed; the markup is stripped out (by design)
- Text widgets—these work in recent WordPress versions
- Email bodies and the like—you need to run
do_shortcode()yourself
Naming considerations
- Avoid short, generic names (
[box]and[button]collide with plugins easily) - Adding a prefix is safer (
[shimaenaga_box]) - Underscores are safer than hyphens
Whichever is registered later wins, so a collision means output you didn’t intend.
How do they fit in the block editor era?
The block editor can do similar things with reusable blocks and synced patterns. Shortcodes are still the right tool in cases like these.
- You want attributes to change the content (count, color, ID)
- PHP processing is needed (fetching posts, calculating something)
- You want to use it outside post content (widgets, templates)
Conversely, when it’s “just paste some fixed HTML,” the patterns feature is kinder to the person editing.
Summary
add_shortcode('name', 'function_name')is all it takes. The function must alwaysreturna string- Receive attributes with
shortcode_atts()and its defaults. The enclosing form uses the second argument,$content - When the HTML is long, build it with
ob_start()plusob_get_clean() - Features you’ll use long-term belong in your own plugin—they survive a theme change
Letting editors place components without writing HTML translates directly into a site that’s easy to run.