Create a WordPress shortcode with attributes the right way

Shortcodes are one of the oldest and most versatile features in WordPress, allowing you to embed dynamic PHP-generated content anywhere a user can type text — in post content, page content, widgets, and even some theme areas. A shortcode is a small token wrapped in square brackets like [my_shortcode] that WordPress replaces with the return value of a registered PHP callback at render time. Shortcodes can accept attributes (similar to HTML attributes) that let editors customise their output without touching code: [button label="Click me" color="blue" url="https://example.com"]. The shortcode_atts() function merges user-supplied attributes with your defaults, making it easy to provide sensible fallbacks. Shortcodes can also be self-closing (like the example above) or enclosing, where they wrap arbitrary content: [highlight]Important text[/highlight]. The enclosed content is passed as the second argument to your callback. Remember to always return output from shortcodes rather than echoing it — if you echo, the content appears at the top of the page before the HTML structure. Shortcodes registered in a plugin are available site-wide, while those in a theme’s functions.php disappear if the theme is changed. For dynamic functionality that also needs to run JavaScript, pair the shortcode with properly enqueued scripts using wp_enqueue_script() inside the callback. This guide covers both self-closing and enclosing shortcodes with attribute validation.

Problem: You want to let editors insert dynamic content or styled HTML into posts using a simple shortcode tag with configurable attributes, without requiring them to write HTML.

Solution: Add the following code to your functions.php file:

/**
 * Self-closing shortcode: [helloadmin_button label="Go" url="https://..." color="blue" size="md"]
 */
add_shortcode( 'helloadmin_button', 'helloadmin_button_shortcode' );
function helloadmin_button_shortcode( $atts ) {
    $atts = shortcode_atts(
        [
            'label' => 'Click here',
            'url'   => '#',
            'color' => 'blue',   // blue | green | red
            'size'  => 'md',     // sm | md | lg
        ],
        $atts,
        'helloadmin_button'
    );

    $allowed_colors = [ 'blue', 'green', 'red' ];
    $allowed_sizes  = [ 'sm', 'md', 'lg' ];

    $color = in_array( $atts['color'], $allowed_colors, true ) ? $atts['color'] : 'blue';
    $size  = in_array( $atts['size'],  $allowed_sizes,  true ) ? $atts['size']  : 'md';

    return sprintf(
        '<a href="%s" class="btn btn-%s btn-%s">%s</a>',
        esc_url( $atts['url'] ),
        esc_attr( $color ),
        esc_attr( $size ),
        esc_html( $atts['label'] )
    );
}

/**
 * Enclosing shortcode: [helloadmin_highlight color="yellow"]Important text[/helloadmin_highlight]
 */
add_shortcode( 'helloadmin_highlight', 'helloadmin_highlight_shortcode' );
function helloadmin_highlight_shortcode( $atts, $content = '' ) {
    $atts = shortcode_atts( [ 'color' => 'yellow' ], $atts, 'helloadmin_highlight' );

    return sprintf(
        '<mark style="background-color:%s;">%s</mark>',
        esc_attr( $atts['color'] ),
        wp_kses_post( $content )
    );
}

/**
 * Use shortcodes inside widget text areas.
 */
add_filter( 'widget_text', 'do_shortcode' );

NOTE: Always sanitise and validate shortcode attributes before using them in output. The example above uses an allowlist (in_array()) for the color and size attributes to prevent arbitrary CSS injection. For URL attributes, always pass them through esc_url(). For text attributes used inside HTML, use esc_attr(). For displayed text, use esc_html(). Never pass raw shortcode attributes directly into SQL queries — if you need to query the database, use $wpdb->prepare().