Tips・The foundations of WordPress customization

How to create shortcodes (add_shortcode)

How to build shortcodes so writing [myform] in an article calls a feature. Covers receiving attributes, the enclosing form, using do_shortcode in templates, and why shortcodes sometimes don't run.

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.

TipsChoosing the right escaping function (esc_html, esc_attr, esc_url)Choosing between escaping functions

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.

TipsHow to fetch posts outside the loop (WP_Query)How to write WP_Query, and how to clean up after it

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

  1. add_shortcode('name', 'function_name') is all it takes. The function must always return a string
  2. Receive attributes with shortcode_atts() and its defaults. The enclosing form uses the second argument, $content
  3. When the HTML is long, build it with ob_start() plus ob_get_clean()
  4. 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.

FAQ

Where do I write shortcodes?
Write add_shortcode('name', 'function_name') in functions.php or in your own plugin. Features you want to keep using across theme changes belong in a plugin (change themes and the shortcode is left showing as plain text).
The shortcode shows up as literal text in my article.
That shortcode name isn't registered. Check the spelling of the function name, when add_shortcode runs (register early, on init or similar), and whether you've switched themes or plugins.
Can I echo inside a shortcode?
No. A shortcode promises to "return a string," so use return. When the HTML is long, capture the output as a string with ob_start() and ob_get_clean(), then return it.
Can I use a shortcode inside a template file?
Yes, with echo do_shortcode('[myform]');. For your own features, though, calling the function directly rather than going through a shortcode is simpler.