Shortcodes
A shortcode is a bracket-delimited tag ([gallery], [my_button]) that expands to HTML at render time. Define with add_shortcode; users embed in posts/pages without touching code.
Register, attributes, content, nesting
EXAMPLE
<?php
// 1) Simplest shortcode
add_shortcode('hello', function () {
return '<p>Hello, world!</p>';
});
// Usage in post / page:
// [hello]
// Renders:
// <p>Hello, world!</p>
// 2) Shortcode with attributes
add_shortcode('greeting', function ($atts) {
$atts = shortcode_atts([
'name' => 'friend',
'style' => 'normal',
], $atts);
return sprintf(
'<p class="greeting greeting--%s">Hello, %s!</p>',
esc_attr($atts['style']),
esc_html($atts['name']),
);
});
// Usage:
// [greeting name="Ada" style="loud"]
// → <p class="greeting greeting--loud">Hello, Ada!</p>
// 3) Shortcode with content (enclosing)
add_shortcode('callout', function ($atts, $content = null) {
$atts = shortcode_atts([
'type' => 'info',
], $atts);
return sprintf(
'<div class="callout callout--%s">%s</div>',
esc_attr($atts['type']),
do_shortcode($content), // process nested shortcodes
);
});
// Usage:
// [callout type="warning"]Watch out![/callout]
// 4) Real-world: button shortcode
add_shortcode('button', function ($atts) {
$atts = shortcode_atts([
'href' => '#',
'label' => 'Click',
'style' => 'primary',
'target' => '_self',
'class' => '',
], $atts);
$classes = trim(sprintf('btn btn-%s %s', $atts['style'], $atts['class']));
return sprintf(
'<a class="%s" href="%s" target="%s" rel="%s">%s</a>',
esc_attr($classes),
esc_url($atts['href']),
esc_attr($atts['target']),
$atts['target'] === '_blank' ? 'noopener noreferrer' : '',
esc_html($atts['label']),
);
});
// Usage:
// [button href="/contact" label="Contact us" style="primary"]
// [button href="https://example.com" label="External" target="_blank"]
// 5) Shortcode that lists recent posts
add_shortcode('recent_posts', function ($atts) {
$atts = shortcode_atts([
'count' => 5,
'category' => '',
'order' => 'desc',
], $atts);
$args = [
'posts_per_page' => intval($atts['count']),
'order' => strtoupper($atts['order']),
];
if (!empty($atts['category'])) {
$args['category_name'] = sanitize_title($atts['category']);
}
$q = new WP_Query($args);
if (!$q->have_posts()) return '<p>No posts found.</p>';
ob_start();
echo '<ul class="recent-posts">';
while ($q->have_posts()) {
$q->the_post();
printf(
'<li><a href="%s">%s</a> <small>%s</small></li>',
esc_url(get_permalink()),
esc_html(get_the_title()),
esc_html(get_the_date()),
);
}
echo '</ul>';
wp_reset_postdata();
return ob_get_clean();
});
// Usage:
// [recent_posts count="10" category="news"]
// 6) Conditional content based on user / context
add_shortcode('logged_in', function ($atts, $content = null) {
if (is_user_logged_in()) {
return do_shortcode($content);
}
return '';
});
add_shortcode('guest', function ($atts, $content = null) {
if (!is_user_logged_in()) {
return do_shortcode($content);
}
return '';
});
// Usage:
// [logged_in]Welcome back, [user_name]![/logged_in]
// [guest][button href="/login" label="Sign in"][/guest]
// 7) Embedded form
add_shortcode('newsletter', function () {
ob_start();
?>
<form action="<?php echo esc_url(admin_url('admin-post.php')); ?>" method="post" class="newsletter-form">
<?php wp_nonce_field('newsletter', 'newsletter_nonce'); ?>
<input type="hidden" name="action" value="newsletter_signup" />
<label for="email">Email</label>
<input id="email" type="email" name="email" required />
<button type="submit">Subscribe</button>
</form>
<?php
return ob_get_clean();
});
// Handle the form submission
add_action('admin_post_nopriv_newsletter_signup', 'handle_newsletter');
add_action('admin_post_newsletter_signup', 'handle_newsletter');
function handle_newsletter() {
if (!isset($_POST['newsletter_nonce']) || !wp_verify_nonce($_POST['newsletter_nonce'], 'newsletter')) {
wp_die('Invalid nonce');
}
$email = sanitize_email($_POST['email']);
// ... add to mailing list ...
wp_redirect(home_url('/thanks'));
exit;
}
// 8) Gallery / list with nested shortcodes
add_shortcode('gallery_grid', function ($atts, $content) {
$atts = shortcode_atts(['cols' => 3], $atts);
return sprintf(
'<div class="gallery-grid cols-%d">%s</div>',
intval($atts['cols']),
do_shortcode($content),
);
});
add_shortcode('gallery_item', function ($atts) {
$atts = shortcode_atts(['src' => '', 'alt' => '', 'caption' => ''], $atts);
return sprintf(
'<figure><img src="%s" alt="%s" /><figcaption>%s</figcaption></figure>',
esc_url($atts['src']),
esc_attr($atts['alt']),
esc_html($atts['caption']),
);
});
// Usage:
// [gallery_grid cols="3"]
// [gallery_item src="/a.jpg" caption="A"]
// [gallery_item src="/b.jpg" caption="B"]
// [gallery_item src="/c.jpg" caption="C"]
// [/gallery_grid]
// 9) Output content from a CPT
add_shortcode('product', function ($atts) {
$atts = shortcode_atts(['id' => 0, 'sku' => ''], $atts);
if ($atts['sku']) {
$query = new WP_Query([
'post_type' => 'product',
'meta_key' => 'sku',
'meta_value' => sanitize_text_field($atts['sku']),
'posts_per_page' => 1,
]);
if (!$query->have_posts()) return '<p>Product not found.</p>';
$query->the_post();
} elseif ($atts['id']) {
$post = get_post(intval($atts['id']));
if (!$post || $post->post_type !== 'product') return '<p>Product not found.</p>';
setup_postdata($post);
} else {
return '<p>Specify id or sku.</p>';
}
ob_start();
$price = get_post_meta(get_the_ID(), 'price', true);
?>
<article class="product-card">
<?php the_post_thumbnail('medium'); ?>
<h3><?php the_title(); ?></h3>
<p class="price">$<?php echo esc_html(number_format((float) $price, 2)); ?></p>
<?php the_excerpt(); ?>
<a href="<?php echo esc_url(get_permalink()); ?>" class="btn">View</a>
</article>
<?php
wp_reset_postdata();
return ob_get_clean();
});
// Usage:
// [product sku="A-100"]
// [product id="123"]
// 10) Programmatic invocation — do_shortcode
echo do_shortcode('[button href="/login" label="Sign in"]');
// 11) Shortcode UI for editors (TinyMCE button) — older approach
// For Gutenberg / block editor — convert shortcodes to blocks instead.
// Wrappers: register_block_type + render via PHP for legacy.
// 12) Disable wpautop wrapping for shortcode output
function remove_wpautop_around_shortcode($content) {
$content = preg_replace('/<p>\\s*(\\[gallery_grid[^\\]]*\\])/i', '$1', $content);
$content = preg_replace('/(\\[\\/gallery_grid\\])\\s*<\\/p>/i', '$1', $content);
return $content;
}
add_filter('the_content', 'remove_wpautop_around_shortcode');
// 13) Security checklist
// ✅ Escape ALL output: esc_html, esc_attr, esc_url, esc_textarea
// ✅ Sanitize input attributes
// ✅ Use prepared statements if querying DB
// ✅ Check capabilities if action is privileged
// ✅ Use nonces for forms (wp_create_nonce, wp_verify_nonce)
// ✅ Prefix function names (mytheme_shortcode_X) to avoid collisions
// 14) Performance
// - Shortcodes run on EVERY render of the_content
// - Cache expensive queries with transients:
// $cache_key = 'recent_posts_' . md5(serialize($atts));
// if (false === ($html = get_transient($cache_key))) {
// // build $html...
// set_transient($cache_key, $html, HOUR_IN_SECONDS);
// }
// return $html;
// - Invalidate on save_post hook
// 15) Gutenberg / Block Editor — modern alternative
// Shortcodes still work in posts, but the block editor encourages CUSTOM BLOCKS.
// Pros of blocks:
// - Visual editing experience
// - Schema-validated attributes
// - Server-side OR client-side rendering
// - Reusable as patterns
// Convert shortcodes → blocks via:
// 1. register_block_type with attributes
// 2. render_callback that calls do_shortcode under the hood
// This preserves backward compatibility while modernising the UX.
// 16) Common bugs
// • Forgetting esc_* → XSS risk
// • Direct $_POST in shortcode handler → CSRF
// • Not calling do_shortcode on nested content → nested shortcodes break
// • Using <p> tags inside shortcode output that conflicts with wpautop
// • Caching without invalidation → stale content forever
// 17) Best practices
// ✅ Always use shortcode_atts with safe defaults
// ✅ Escape output religiously
// ✅ Sanitize attributes (sanitize_text_field, intval, esc_url_raw)
// ✅ Return strings, don't echo (shortcodes RETURN)
// ✅ Prefix function + shortcode names
// ✅ Document attributes inline
// ✅ Consider blocks for new features in modern WP
Why it matters
Shortcodes are the classic WP extension point — user-friendly, theme-portable, easy to write. For new features in modern WP, consider blocks: same dynamic rendering, better UX in the editor, schema-validated attributes.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
add_shortcode('button', function ($atts) {
$atts = shortcode_atts(['color' => '#04AA6D'], $atts);
return '<a style="background:' . $atts['color'] . '">Click</a>';
});
Try it Yourself »
Exercise
Register a shortcode named hello.
('hello', 'render_hello');
add_shortcode.
Discussion
Loading…