Skip to content
⚡ How to add your own shortcode in WordPress: examples and code

⚡ How to add your own shortcode in WordPress: examples and code

You need to output the last updated date of a post in the footer of every article. Or insert a subscription form exactly in the middle of the text, without a code editor and template gymnastics. In WordPress, shortcodes are the tool for this.

A shortcode is a short tag in square brackets: [myshortcode]. WordPress finds it in the content and replaces it with the result of a PHP function. You use standard shortcodes like [gallery] and [embed] all the time without even thinking about it. But your own shortcode is a lever: you write the logic once, and the editor inserts it anywhere with three clicks.

In 10 minutes you will create your first working shortcode: from an empty function to a tag with attributes and enclosed content. The code is real, runs on any modern WordPress, and has been tested on a staging site.

💡 Quick overview:

  • The add_shortcode function: what it accepts and how it works internally
  • A basic [current_year] shortcode, the current year in text
  • A shortcode with attributes [cta text="Скачать"], a custom button
  • A shortcode with enclosed content [notice]Текст[/notice], a styled block
  • Adding via a plugin or functions.php: pros and cons of each approach
  • Common mistakes: echo instead of return, name conflicts, empty attribute in PHP 8+

What add_shortcode can do and how it works

The add_shortcode function is the only standard way to register a new shortcode in WordPress. It accepts two parameters:

  • $tag (string), the shortcode name you will write in square brackets. Only letters, numbers, and underscores. Spaces and special characters & / < > [ ] = are forbidden; WordPress will silently refuse to register such a tag.
  • $callback (callable), the name of the PHP function that will fire when the shortcode is found. This function generates the HTML that the visitor will see.

The function's source code, five lines of logic and two sanity checks (full listing at developer.wordpress.org):

1function add_shortcode( $tag, $callback ) {
2 global $shortcode_tags;
3
4 if ( '' === trim( $tag ) ) {
5 _doing_it_wrong(
6 __FUNCTION__,
7 __( 'Invalid shortcode name: Empty name given.' ),
8 '4.4.0'
9 );
10 return;
11 }
12
13 if ( 0 !== preg_match( '@[<>&/\[\]\x00-\x20=]@', $tag ) ) {
14 _doing_it_wrong(
15 __FUNCTION__,
16 sprintf(
17 /* translators: 1: Shortcode name, 2: Space-separated list of reserved characters. */
18 __( 'Invalid shortcode name: %1$s. Do not use spaces or reserved characters: %2$s' ),
19 $tag,
20 '& / < > [ ] ='
21 ),
22 '4.4.0'
23 );
24 return;
25 }
26
27 $shortcode_tags[ $tag ] = $callback;
28}

The key point: $shortcode_tags, a global array. WordPress stores all registered shortcodes in it. During content rendering, the core iterates over this array and calls the associated function for each tag found. Two implications follow:

  • Register on the init hook. If you call add_shortcode directly in a plugin file, it may fire before WordPress is fully initialized. The correct way is to wrap it in a hook:
1add_action( 'init', 'my_register_shortcodes' );
2
3function my_register_shortcodes() {
4 add_shortcode( 'mytag', 'my_shortcode_callback' );
5}
  • A prefix is mandatory. The global array is shared by all plugins and the theme. If you name a shortcode [button], your [button] silently overwrites the same shortcode from a forms plugin. A prefix like myplugin_ or a unique abbreviation solves the problem. In the examples below I use the mysite_ prefix; in a real project, replace it with your own.

Step 1: a simple shortcode without parameters

The shortest path to a working shortcode, three lines in your child theme's functions.php (or in the Code Snippets plugin, which is safer and won't be lost on theme update).

Add this code and save:

1add_shortcode( 'mysite_current_year', 'mysite_display_current_year' );
2
3function mysite_display_current_year() {
4 return date( 'Y' );
5}

Now write [mysite_current_year] in any post or page. On the front end, the current year will appear instead of the shortcode: "2026".

What happens here. The mysite_display_current_year function takes no arguments, calls the PHP function date('Y'), and returns a four-digit string. WordPress inserts this string exactly where the shortcode was placed. No magic.

Where to apply this in practice: the year in the footer (© [mysite_current_year]), the project age in text ("working since [mysite_current_year]"), automatic updating of dates on legal compliance pages.

Step 2: a shortcode with attributes

A shortcode without parameters is useful, but real flexibility starts with attributes. The classic example, a button with customizable text and a link:

1add_shortcode( 'mysite_cta', 'mysite_cta_button' );
2
3function mysite_cta_button( $atts ) {
4 $atts = shortcode_atts(
5 array(
6 'text' => 'Learn more',
7 'url' => '#',
8 ),
9 $atts,
10 'mysite_cta'
11 );
12
13 return sprintf(
14 '<a href="%s" class="mysite-cta-button">%s</a>',
15 esc_url( $atts['url'] ),
16 esc_html( $atts['text'] )
17 );
18}

In the editor, use it like this:

1[mysite_cta text="Download plugin" url="https://example.com/myplugin/"]

The shortcode_atts function does three things at once: it merges user attributes with default values, filters only known keys, and normalizes an empty string into an array (without it, calling [mysite_cta] with no attributes in PHP 8+ would throw a TypeError, because the first parameter of the callback function would receive an empty string instead of an array).

*Why esc_url and esc_html.* A shortcode is inserted by an editor, today that is you, and tomorrow a content manager without code access. Output escaping insures against accidental XSS if a bracket or tag ends up in the text attribute.

Step 3: a shortcode with enclosed content

Some shortcodes wrap a fragment of text: [mysite_notice]Важное сообщение[/mysite_notice]. The enclosed content arrives in the callback as the second parameter, $content. A typical scenario, a styled warning block:

1add_shortcode( 'mysite_notice', 'mysite_notice_box' );
2
3function mysite_notice_box( $atts, $content = null ) {
4 $atts = shortcode_atts(
5 array(
6 'type' => 'info',
7 ),
8 $atts,
9 'mysite_notice'
10 );
11
12 $class = 'notice-' . esc_attr( $atts['type'] );
13
14 return sprintf(
15 '<div class="mysite-notice %s"><p>%s</p></div>',
16 $class,
17 do_shortcode( $content )
18 );
19}

Note the do_shortcode( $content ). If the editor inserted another shortcode inside your shortcode (for example [mysite_current_year]), this wrapper will run it as well. Without do_shortcode, nested shortcodes will display as text in square brackets, raw [mysite_current_year] instead of "2026".

CSS for the block, minimal, to get started:

1.mysite-notice {
2 border-left: 4px solid #2271b1;
3 background: #f0f6fc;
4 padding: 1em 1.2em;
5 margin: 1.5em 0;
6 border-radius: 4px;
7}
8.mysite-notice.notice-warning {
9 border-left-color: #dba617;
10 background: #fcf9e8;
11}

Add the styles to your child theme’s style.css or via Appearance → Customize → Additional CSS.

Step 4: a shortcode inside a plugin (OOP approach)

When you have more than three shortcodes, functions.php turns into a dumping ground. It is time to move the logic into a separate plugin, and ideally into a class. Here is a mini-plugin skeleton with one shortcode:

1<?php
2/**
3 * Plugin Name: MySite Shortcodes
4 * Description: Custom shortcodes for the site.
5 * Version: 1.0.0
6 * Requires PHP: 7.4
7 */
8
9defined( 'ABSPATH' ) || exit;
10
11class MySite_Shortcodes {
12
13 public static function init() {
14 add_action( 'init', array( __CLASS__, 'register' ) );
15 }
16
17 public static function register() {
18 add_shortcode( 'mysite_email', array( __CLASS__, 'email_obfuscated' ) );
19 }
20
21 public static function email_obfuscated( $atts ) {
22 $atts = shortcode_atts(
23 array( 'address' => '' ),
24 $atts,
25 'mysite_email'
26 );
27
28 if ( ! is_email( $atts['address'] ) ) {
29 return '';
30 }
31
32 return sprintf(
33 '<a href="mailto:%1$s">%1$s</a>',
34 antispambot( $atts['address'], 1 )
35 );
36 }
37}
38
39MySite_Shortcodes::init();

Place this file in wp-content/plugins/mysite-shortcodes/mysite-shortcodes.php and activate the plugin in the admin panel. The shortcode [mysite_email address="hello@example.com"] will output a spam-bot-protected link; the antispambot function encodes email characters into HTML entities.

Why a class, not a set of functions. Namespacing: three shortcodes in a class will not collide with third-party functions. Plus autoloading, plus readable code if the plugin grows to a dozen shortcodes.

Testing a shortcode before publishing

After adding the code, check three scenarios:

  • Shortcode without attributes. Just [mysite_cta], it should render with default values (text "Learn more", link #).
  • Shortcode with attributes. A full set of parameters, all values are picked up and displayed correctly.
  • Error in attributes. An invalid email in [mysite_email], the function silently returns an empty string instead of breaking the page.

Important: never use echo inside a callback function. A shortcode must return a string via return. If you output HTML with echo, it will appear not where the shortcode is, but at the very top of the page, because WordPress runs shortcode rendering before content output. The same logic as filters: the function hands back a value, and the engine itself decides where to insert it.

A second nuance: if the shortcode does not display (you see [mysite_cta] as text, not a button), check that the function is registered on the init hook, not directly in the plugin body. Without init, the global $shortcode_tags array may not yet be ready to accept new tags.

Shortcode in a theme vs. a plugin: what to choose

Criterion

Theme functions.php

Separate plugin

Startup speed

Faster - file is already loaded

Slightly slower - separate file

Portability

Tied to the theme

Works with any theme

Update survivability

Lost on theme update

Lives independently

Editing convenience

Appearance → Theme File Editor

Plugin editor or FTP

For how many shortcodes

1-3

4+

The rule: start with functions.php on a test site, build three shortcodes, then move them into a plugin. A theme update six months later will not bury your logic, and you can transfer the shortcodes to another project in a minute.

⁉️🤔 FAQ

What is the difference between a shortcode and a Gutenberg block?

A shortcode is a text tag in square brackets that is processed on the server side. A block is a visual editor component, a React component with settings in the sidebar. Shortcodes appeared in WordPress 2.5 (2008), blocks in 5.0 (2018). Today blocks are the primary way to insert dynamic content, but shortcodes remain relevant: they are simpler to develop, require no JavaScript knowledge, and work in any editor, including the Classic Editor and page builders.

A shortcode is a text tag that WordPress replaces with the result of a PHP function when rendering the page. Unlike a Gutenberg block, a shortcode has no visual interface in the editor: the content manager writes [myshortcode] as text, and sees the finished HTML on the front end.

Can I use a shortcode inside another shortcode?

Yes. If the outer shortcode's callback function wraps the nested content in do_shortcode(), the inner shortcodes will work correctly: [notice][current_year][/notice] will output a styled block with the current year. Without do_shortcode(), the nested shortcode will remain as text in square brackets.

Nested shortcodes are processed recursively: WordPress goes through the string multiple times until no unprocessed tags remain. But for this to work, the outer shortcode must explicitly call do_shortcode($content). Otherwise the string [current_year] will stay as text instead of turning into "2026".

Why is my shortcode not working and showing as text in square brackets?

Three common reasons. First: add_shortcode is called before the init hook, move the registration inside add_action('init', ...). Second: a typo in the tag name, [my_shortcode] in the editor but myshortcode is registered. Third: the callback function uses echo instead of return, so the output goes to the top of the page and the shortcode location is empty.

The most frequent culprit is echo instead of return in the callback function. WordPress calls shortcode handlers before outputting the main content, so the echo result hits the output buffer earlier than the page header. Open the page source (Ctrl+U): if you see the shortcode's HTML at the very top, before <html>, that is exactly the problem.

Do I need to escape shortcode output?

Absolutely. A shortcode accepts attributes from the editor, meaning potentially from any user with author or editor permissions. esc_html() for text, esc_url() for links, esc_attr() for HTML attributes. The exception is when you intentionally return HTML markup (like <div class="notice">). But even then, escape attributes within the markup.

Yes, escaping is mandatory for everything coming from shortcode attributes or user input. Even if today only you insert shortcodes, tomorrow a content manager will get editor access. esc_html() for text, esc_url() for links, esc_attr() for HTML attributes protect against accidental or intentional XSS.

How many shortcodes can I register on one site?

There is no technical limit: $shortcode_tags is a regular PHP array, you will hit the server memory limit far later than common sense. In practice, after 15-20 custom shortcodes it is worth asking: is it time to move some of them to Gutenberg blocks? Keep shortcodes with unique logic (email obfuscation, conditional output by role). For shortcodes that just style text ([highlight]), it is better to use a block style or a CSS class.

Technically, as many as you want: the $shortcode_tags array has no artificial limit. Practically, more than 20-25 shortcodes on one site suggests that some of the logic should be moved to blocks. Each shortcode adds content parsing overhead: before output, WordPress iterates over the entire array of registered tags and searches for matches in the post text.

Should I write my own shortcodes or are ready-made plugins enough?

A ready-made plugin covers most typical tasks: forms, Contact Form 7, tables, TablePress, grids, any page builder. A custom shortcode is needed when the logic is specific to your site and no ready-made solution exists: outputting a custom field exactly at this spot on the page, the date of the next webinar from the database, a personalized greeting based on user role.

In short: for standard functionality, use a plugin and do not code. For unique logic, add_shortcode + functions.php or a mini-plugin. The code in any of the examples above is under ten lines. Once you master these three patterns (simple, with attributes, with nested content), you will cover the vast majority of tasks for which custom shortcodes are written at all.