You write [galeria] in a post, you publish, and in its place, a complete image gallery appears. That little magic is WordPress shortcodes: text shortcuts enclosed in square brackets that the system replaces with dynamic content generated with PHP. They have been in WordPress since version 2.5 and, although the block editor has taken over some of their territory, they remain an enormously practical tool: they are used by form plugins, pricing tables, booking systems, and thousands of themes. And the best part is that creating your own is surprisingly simple. In this guide, we will see what WordPress shortcodes are and how they work internally, how to use the ones you already have installed, and above all, how to program your own shortcodes step by step: simple, with attributes, with nested content, and even with template output. All with complete and functional PHP code that you can copy, adapt, and use today.
What WordPress shortcodes are and how they work
A shortcode is a short tag enclosed in square brackets, like [contact-form-7] o [woocommerce_cart], which WordPress searches for in the content just before displaying it. When found, it executes the associated PHP function and replaces the tag with whatever that function returns. The process occurs on each page load, on the filter the_content, so the text saved in the database remains [galeria]: the substitution is always dynamic.
There are three ways to write a shortcode:
- Simple:
[mi_shortcode]— is replaced as is. - With attributes:
[mi_shortcode color="rojo" numero="5"]— receives parameters that modify the output. - With content:
[aviso]Texto interior[/aviso]— wraps content that the function can process.
WordPress includes several by default:
,
,
,
o . Plugins add their own: each Contact Form 7 form, for example, generates a unique shortcode that you paste wherever you want to display it. If a shortcode appears as plain text on your website (brackets included), it almost always means that the plugin that interpreted it is deactivated.
How to use shortcodes in posts, pages, and widgets
In the block editor (Gutenberg) there is a specific block called «Shortcode»: add it and paste the tag inside. In reality, they also work pasted into a paragraph block, but the dedicated block makes it clearer what is content and what is code. In the classic editor, simply type them into the text.
Outside the main content, you have two ways:
- Widgets: text and custom HTML widgets process shortcodes since WordPress 4.9 without the need for extra code.
- Template files: if you need to execute a shortcode within a theme's PHP file, use the function
do_shortcode():
<?php
// En un archivo de plantilla del tema (por ejemplo, footer.php):
echo do_shortcode( '[mi_formulario id="3"]' );
It is advisable not to overuse do_shortcode() in templates: if you control the code, calling the shortcode's PHP function directly is faster. But as a one-off resource, it is perfectly valid.
How to create your first shortcode step by step
The entire API revolves around one function: add_shortcode(). It receives the tag name and the callback function that generates the output. The golden rule: the callback returns the content with return, never prints it with echo. If you echo, the content will appear out of place (usually at the beginning of the post).
Let's start with the most useful of all simple shortcodes: the current year, perfect for copyright notices that never become outdated:
<?php
function biblioweb_shortcode_anio() {
return date_i18n( 'Y' );
}
add_shortcode( 'anio', 'biblioweb_shortcode_anio' );
// Uso: © [anio] Mi Empresa
Registration must be done when WordPress is already loaded, so the correct way is to hook it to init. If you don't yet master the actions and filters system, we recommend strengthening that foundation first, because WordPress shortcodes rely directly on it:
<?php
function biblioweb_registrar_shortcodes() {
add_shortcode( 'anio', 'biblioweb_shortcode_anio' );
add_shortcode( 'aviso', 'biblioweb_shortcode_aviso' );
}
add_action( 'init', 'biblioweb_registrar_shortcodes' );
Where to place this code? The same three options as always: the functions.php of a child theme, a custom plugin, or, the most convenient way for most, a snippet manager like WPCode, the snippet manager we analyze in detail, which validates the PHP before saving it and allows activating and deactivating each shortcode individually.
Shortcodes with attributes: parameters that change the output
Attributes turn a fixed shortcode into a reusable piece. The callback receives them in its first parameter ($atts), and the function shortcode_atts() it is responsible for merging them with the default values, discarding any unforeseen attributes:
<?php
function biblioweb_shortcode_boton( $atts ) {
$datos = shortcode_atts(
array(
'url' => '#',
'texto' => 'Más información',
'color' => '#0057d9',
),
$atts,
'boton'
);
return sprintf(
'<a class="bw-boton" href="%s" style="background:%s">%s</a>',
esc_url( $datos['url'] ),
esc_attr( $datos['color'] ),
esc_html( $datos['texto'] )
);
}
add_shortcode( 'boton', 'biblioweb_shortcode_boton' );
// Uso: [boton url="https://ejemplo.com" texto="Descargar guía" color="#1d9e75"]
Three important details that this example applies and that distinguish a professional shortcode from a fragile one:
- Escaping the output:
esc_url(),esc_attr()yesc_html()prevent a malicious attribute from injecting HTML or JavaScript. - Default values: the shortcode works even if the user doesn't pass any attributes.
- Third parameter of
shortcode_atts(): the shortcode name activates the filtershortcode_atts_boton, which allows other developers to modify the default values.
A practical note: WordPress converts attribute names to lowercase, so always use mi_atributo and not miAtributo when reading them in the callback.
Shortcodes with content and nested shortcodes
When the shortcode wraps text ([aviso]...[/aviso]), that content arrives at the callback as a second parameter. The classic use case is a highlighted alert box:
<?php
function biblioweb_shortcode_aviso( $atts, $content = null ) {
$datos = shortcode_atts(
array( 'tipo' => 'info' ),
$atts,
'aviso'
);
$tipos = array( 'info', 'exito', 'alerta' );
$tipo = in_array( $datos['tipo'], $tipos, true ) ? $datos['tipo'] : 'info';
return '<div class="bw-aviso bw-aviso--' . esc_attr( $tipo ) . '">'
. do_shortcode( wp_kses_post( $content ) )
. '</div>';
}
add_shortcode( 'aviso', 'biblioweb_shortcode_aviso' );
// Uso: [aviso tipo="alerta"]Haz una copia de seguridad [anio] antes de continuar.[/aviso]
Two functions do the fine work: wp_kses_post() sanitizes the content, allowing only HTML specific to a post, and do_shortcode() applied over $content makes sure that nested shortcodes (like the [anio] of the example) are also processed. Without that call, the inner brackets would appear as is.
With the same technique, you can build more ambitious structures, such as columns ([fila][columna]...[/columna][/fila]) or tabs, although for pure layout, the block editor is usually a better tool today. The strong point of the shortcode remains dynamic logic: showing content only to registered users, rendering data from an API, listing related posts.
A complete example: recent posts list with cache
Let's close the technical part with a realistic shortcode that combines attributes, a database query, and a caching layer with transients to avoid penalizing performance:
<?php
function biblioweb_shortcode_recientes( $atts ) {
$datos = shortcode_atts(
array(
'numero' => 5,
'categoria' => '',
),
$atts,
'recientes'
);
$clave = 'bw_recientes_' . md5( serialize( $datos ) );
$html = get_transient( $clave );
if ( false !== $html ) {
return $html;
}
$args = array(
'posts_per_page' => absint( $datos['numero'] ),
'ignore_sticky_posts' => true,
'no_found_rows' => true,
);
if ( '' !== $datos['categoria'] ) {
$args['category_name'] = sanitize_title( $datos['categoria'] );
}
$consulta = new WP_Query( $args );
if ( ! $consulta->have_posts() ) {
return '';
}
$html = '<ul class="bw-recientes">';
while ( $consulta->have_posts() ) {
$consulta->the_post();
$html .= '<li><a href="' . esc_url( get_permalink() ) . '">'
. esc_html( get_the_title() ) . '</a></li>';
}
$html .= '</ul>';
wp_reset_postdata();
set_transient( $clave, $html, HOUR_IN_SECONDS );
return $html;
}
add_shortcode( 'recientes', 'biblioweb_shortcode_recientes' );
// Uso: [recientes numero="3" categoria="wordpress"]
This pattern (query, build HTML, cache for an hour) is applicable to almost any shortcode that makes queries. If one day the listing doesn't update immediately, remember that the transient expires in an hour or can be deleted when saving posts with an action on save_post.
Best practices and errors to avoid
- Return, never echo. The number one error: the output appears at the beginning of the post instead of where you placed the shortcode.
- Prefix the names.
[boton]can clash with that of another plugin;[bw_boton]no. The last oneadd_shortcode()registered wins, and the conflict is silent. - Always escape and sanitize. Attributes and content are user input:
esc_html(),esc_url(),esc_attr()ywp_kses_post()are your seatbelt. - Avoid heavy logic without caching. The shortcode runs on every page load; transients are your allies.
- Don't delete the plugin without cleaning up. If you deactivate the plugin that registers a shortcode, the text with brackets will remain visible. Before removing shortcodes from a project, find and replace them.
- Document the attributes. Six months from now you won't remember if it was
numeroocantidad.
The complete API reference, with all the details of the parser and its limitations, is in the official shortcode documentation from developer.wordpress.org. And if you're putting together your plugin toolbox, our selection of essential plugins to get started with WordPress will help you cover the rest of the basics.
Frequently Asked Questions
Are shortcodes obsolete with the block editor?
No. Gutenberg has replaced their use for layout (columns, buttons, galleries), but WordPress shortcodes are still the standard mechanism for many plugins and the fastest way to insert reusable dynamic logic. In fact, the editor includes a specific block for them, and converting a shortcode into a native block is a considerably more complex project.
Why does my shortcode appear as text with brackets?
Three common causes: the plugin or snippet that registers it is deactivated, the name is misspelled (names distinguish hyphens and underscores), or you have placed it in an area where shortcodes are not processed, such as some old widget fields or titles. First, check that the registration code is executing.
Can I use a shortcode inside another?
Yes, as long as the outer shortcode processes its content with do_shortcode( $content ) in its callback, as we saw in the alert box example. With a parser limitation: two shortcodes with the same name cannot be nested within each other.
Shortcode or custom block: which should I create?
If you need a quick, reusable solution that also works in widgets, templates, or the classic editor, the shortcode wins for simplicity: a PHP function and you're done. If you're looking for a visual experience in the editor, with real-time preview and graphical controls, the native block is superior, at the cost of requiring JavaScript and a build process.
Conclusion
WordPress shortcodes are the most direct path between a need ("I want to show this here") and a dynamic PHP solution: a tag in brackets, a function that returns HTML, and the system does the rest. We've seen how to use them in posts, widgets, and templates, how to create them from scratch with add_shortcode(), how to make them flexible with attributes and default values, how to safely process nested content, and how to cache their output so they don't impact performance. With the patterns in this guide (return instead of echo, systematic escaping, name prefixes, and transients for queries) you have everything you need to build solid and professional shortcodes. Start with the simplest one, the copyright year, and work your way up: in no time you'll have your own library of reusable utilities for any project.