Skip to content

Everything for WordPress, web development — and beyond

🔘 Button shortcode in WordPress: ready-to-use PHP code and modern CSS 2026

🔘 Button shortcode in WordPress: ready-to-use PHP code and modern CSS 2026

You need a "Download" button in the middle of an article, but your theme only shows a blue underlined link. Sound familiar? In Block Editor a button is added with one click, but in the classic editor, in widgets, and in custom themes you have to write HTML with classes by hand every time.

Copying markup from post to post ends predictably: you forget a class in one place, lose rel="noopener" in another, and the button is suddenly a different shade in a third. The more content your site has, the more expensive each style change becomes.

The solution takes about ten minutes. Below you will find a ready-made [button] PHP shortcode, an OKLCH palette, dark theme support via light-dark(), and WCAG 2.2 accessibility. Everything has been tested in current 2026 browsers.

How to add a button via shortcode: action plan

💡 Quick overview:

  • Step 1: Add the button PHP function to your child theme's functions.php
  • Step 2: Register the [button] tag with add_shortcode() on the init hook
  • Step 3: Include CSS with all states: default, hover, active, focus
  • Step 4: Define color schemes with a single OKLCH line per color
  • Step 5: Insert [button url="..." color="blue"]Text[/button] in a post and test keyboard focus

Each step below includes ready-to-copy code. Place the code in a child theme so that updating the parent theme does not erase your changes. If you do not have a child theme yet, create one first using the official WordPress guide to child themes; it takes only a few minutes.

But before copying, take a look at what has changed in button styling itself. Over the past few years CSS for buttons has been almost completely rewritten.

What changed in button CSS by 2026

In short: you no longer build buttons out of vendor prefixes and three sets of hand-picked hover shades. Custom properties, native nesting, the OKLCH color model, and the light-dark() function cut a typical style block roughly in half and eliminate duplication.

These techniques apply at enormous scale. According to W3Techs data from July 2026, WordPress powers 41.5% of all websites and holds 59.2% of the CMS market. Any pattern from this article can be carried from project to project for years.

Technique

How it was done before

How it is done in 2026

Color

HEX plus manual shades for hover and active

oklch() plus color-mix() computes shades automatically

Palette

Nine CSS rules per color

One line overriding --btn-bg

Prefixes

-webkit- and -moz- on every property

Not needed: properties have long been standardized

Nesting

Only via Sass or Less

Native CSS nesting

Focus

outline: none and loss of keyboard access

:focus-visible with a visible ring

Dark theme

Separate stylesheet

light-dark() in a single property

Let's start with the server side: until we have a PHP function, there is nothing to style.

Button PHP function

The function accepts attributes from the editor and returns a ready-made <a> tag with classes, a link, and text. Shortcodes appeared back in WordPress 2.5 and remain the fastest way to insert arbitrary markup into content: add_shortcode() from the official reference registers the tag, and a callback assembles the HTML.

1/**
2 * Generates button HTML via the [button] shortcode.
3 *
4 * @param array|string $atts Shortcode attributes.
5 * @param string|null $content Text inside the paired [button]...[/button] tag.
6 * @return string Button HTML markup.
7 */
8function myprefix_button_shortcode( $atts, $content = null ) {
9 $atts = shortcode_atts(
10 array(
11 'url' => '',
12 'title' => '',
13 'target' => '',
14 'text' => '',
15 'color' => 'green',
16 ),
17 $atts,
18 'button'
19 );
20
21 // Button text: text attribute takes priority, then tag content
22 $label = $atts['text'] ? $atts['text'] : $content;
23
24 // URL provided: build <a>
25 if ( $atts['url'] ) {
26 $target_attr = ( 'blank' === $atts['target'] ) ? ' target="_blank" rel="noopener noreferrer"' : '';
27 $title_attr = $atts['title'] ? ' title="' . esc_attr( $atts['title'] ) . '"' : '';
28
29 return sprintf(
30 '<a href="%s" class="myprefix-button color-%s"%s%s>%s</a>',
31 esc_url( $atts['url'] ),
32 esc_attr( $atts['color'] ),
33 $target_attr,
34 $title_attr,
35 do_shortcode( $label )
36 );
37 }
38
39 // No URL: wrap in <span>
40 return sprintf(
41 '<span class="myprefix-button color-%s">%s</span>',
42 esc_attr( $atts['color'] ),
43 do_shortcode( $label )
44 );
45}

Note three details. shortcode_atts() merges provided attributes with defaults, so there is no extract() or undeclared variables in scope. All dynamic values pass through esc_url() and esc_attr(): WordPress does not escape shortcode output for you. And do_shortcode() inside the label allows nesting one shortcode inside another (for example, an icon inside the button).

Registering the shortcode and using it in the editor

Registration hooks into init: by that point the core is fully loaded, and other plugins can override the tag via remove_shortcode() if needed.

1add_action( 'init', 'myprefix_register_button_shortcode' );
2
3function myprefix_register_button_shortcode() {
4 add_shortcode( 'button', 'myprefix_button_shortcode' );
5}

Now open a post and insert the tag. In the classic editor drop it directly into the text; in Block Editor use the "Shortcode" block:

1[button url="https://example.com/download" target="blank" text="Download free"]
2
3[button url="https://example.com" color="blue"]Learn more[/button]

The first variant is self-closing with the text in an attribute. The second is paired: the label sits between opening and closing tags. The result is identical; choose whichever syntax is more convenient for your authors.

Without styles the button still looks like an ordinary link. The mechanics already work, though: the class myprefix-button color-green is in the markup, and all that remains is to style it.

Base styles: custom properties and nesting

The entire appearance rests on a single variable, --btn-bg. color-mix() calculates shades for hover and active from it, so you no longer need to pick darker variants by hand. Nested rules work natively, without Sass.

1.myprefix-button {
2 --btn-bg: oklch(58% 0.15 145);
3 --btn-fg: #fff;
4
5 display: inline-block;
6 padding: 12px 24px;
7 background: var(--btn-bg);
8 color: var(--btn-fg);
9 font-weight: 600;
10 text-decoration: none;
11 border-radius: 8px;
12 cursor: pointer;
13 transition: background 0.2s ease, translate 0.15s ease;
14
15 &:hover {
16 background: color-mix(in oklch, var(--btn-bg), black 12%);
17 color: var(--btn-fg);
18 text-decoration: none;
19 }
20
21 &:active {
22 background: color-mix(in oklch, var(--btn-bg), black 20%);
23 translate: 0 1px;
24 }
25
26 &:focus-visible {
27 outline: 3px solid color-mix(in oklch, var(--btn-bg), white 30%);
28 outline-offset: 2px;
29 }
30}

The translate: 0 1px shift on active mimics a physical button press more subtly than the old inset-shadow technique. transition sets the smoothness, and the text color is explicitly repeated on :hover so that theme styles do not override it.

Copy the block into your child theme's style.css or into the "Additional CSS" section of the Customizer. Now the fun part: color schemes.

OKLCH palette: a new color in one line

Each scheme now takes exactly one line: only the variable changes, and all states are recalculated automatically.

1.myprefix-button.color-blue { --btn-bg: oklch(55% 0.17 255); }
2.myprefix-button.color-red { --btn-bg: oklch(55% 0.19 25); }
3.myprefix-button.color-orange { --btn-bg: oklch(68% 0.16 60); }

Why OKLCH instead of familiar HEX? In the oklch() description on MDN, the first parameter is perceived lightness: two colors with the same first number look equally bright. The blue and red buttons above are visually equivalent; HEX offers no such guarantee. Creating new schemes is easy by changing only the third parameter (hue).

Usage in the editor stays the same: color="red" activates the red scheme, color="orange" the orange one. The default attribute remains green, as in the original function.

Style source code on a developer&#39;s laptop screen

Accessibility: what breaks most often

The direct answer: a button needs a visible focus ring, a sufficient target size, and respect for animation preferences. These are not checkbox requirements. An audit of 17.2 million sites in the Web Almanac 2025 showed that 67% of sites remove the focus outline, and only 30% meet WCAG contrast standards.

The legal framework has also tightened: as of June 28, 2025, the European Accessibility Act applies, and accessibility requirements for digital products in the EU are now a legal norm rather than a recommendation.

Criterion 2.5.8 Target Size (Minimum) at level AA from WCAG 2.2 requires an interactive target to measure at least 24 by 24 CSS pixels or to have an equivalent clear space around it.

Our button with padding: 12px 24px passes the minimum with room to spare, but two rules are worth adding separately:

1.myprefix-button {
2 min-block-size: 44px;
3 align-content: center;
4}
5
6@media (prefers-reduced-motion: reduce) {
7 .myprefix-button {
8 transition: none;
9 }
10}

The first rule brings the height up to a comfortable touch zone on mobile, where missed taps on small targets are especially frustrating. The second disables animations for people who have requested it in their system settings. We have already defined the focus ring via :focus-visible: it appears during keyboard navigation and does not bother mouse users.

Developer checking interface accessibility in a modern office

Dark theme via light-dark()

One function replaces the entire prefers-color-scheme media query. You specify two values (light and dark), and the browser picks the right one based on the active scheme. According to MDN, the light-dark() function has Baseline Newly available status as of May 2024, meaning it works in all current browsers.

1:root {
2 color-scheme: light dark;
3}
4
5.myprefix-button {
6 --btn-bg: light-dark(oklch(58% 0.15 145), oklch(70% 0.13 145));
7 --btn-fg: light-dark(#fff, oklch(22% 0.02 145));
8}

The line color-scheme: light dark is required: without it the function will not activate. For the dark scheme the background is made lighter and the text darker: on a dark canvas saturated dark buttons sink in, a common mistake. A detailed breakdown of theming with all pitfalls is available in the web.dev article on color themes.

If your audience uses old corporate browsers, wrap the new features in @supports and keep plain HEX as a fallback: the button degrades gracefully, losing only shade precision.

Security: three lines people forget

First things first: target="_blank" without rel="noopener noreferrer" opens a tabnabbing vulnerability. The page the button links to gains access to window.opener and can replace the tab containing your site. In our function the attribute is added automatically, but it is easy to lose during modifications.

Escaping is equally important. esc_url() blocks dangerous protocols like javascript:, and esc_attr() prevents breaking out of an attribute. These calls must never be removed for brevity in any shortcode that accepts data from the editor.

One last point: $content is intentionally not passed through wp_kses_post() so that nested shortcodes work. If, however, you allow buttons to be inserted via front-end forms, wrap $label in wp_kses_post() before output.

How to test a button before publishing

A quick test takes a couple of minutes and catches almost every issue users later report. Go through this list before placing the button in production posts:

  • Click the button with a mouse and verify that the link opens in the correct tab.
  • Tab through the page: the focus ring should be obvious at first glance.
  • Enable dark-scheme emulation in DevTools (Rendering panel, prefers-color-scheme option) and check both background variants.
  • Enable prefers-reduced-motion emulation in the same panel and confirm that the button stops animating.
  • Open the page in mobile mode and try tapping the button with your thumb.

If everything passes, the shortcode is ready for mass use. Make a habit of running through this list after every style change: regressions in focus states and dark theme are invisible until you specifically look for them. The most common questions about shortcode behavior are collected below.

⁉️🤔 Frequently asked questions

Does this shortcode work in Block Editor?

Yes, fully. Insert a "Shortcode" block and type [button ...] inside it. The markup is assembled on the server when the post is rendered, so the editor does not care how the tag got into the content. For frequently used elements it eventually makes sense to register a native block via register_block_type(), but that is a separate project.

What if a visitor's browser does not understand oklch() or light-dark()?

Wrap modern values in an @supports directive and keep plain HEX as the default value. An old browser will apply the fallback color; a new one will use OKLCH. The button remains functional in both cases: graceful degradation affects only shade precision and dark-theme automation, not functionality.

Why does the shortcode appear on the page as text in square brackets?

Most often the function is not registered: verify that functions.php saved without syntax errors and that the init hook fired. A second possibility: you inserted the tag in a template file, where you need an explicit echo do_shortcode( '[button ...]' ) call. Enable WP_DEBUG and check the error log.

How do I add an icon inside the button?

The quickest route: an emoji right in the text, for example text="📥 Download". For SVG pass the file URL as a separate attribute and output an <img> tag inside the link via the same sprintf. The Dashicons approach also works: a ::before pseudo-element plus font-family: dashicons in a dedicated class.

Are vendor prefixes for border-radius and transition still needed in 2026?

No. These properties have long been standardized and work in all current browsers without prefixes. Check support for a specific feature in MDN tables or on caniuse.com, which also show the Baseline status indicating when a feature can be used without fallbacks.

💎 Summary and conclusions

For a site that needs only a couple of standard buttons, a custom shortcode beats any plugin: full control over markup, zero extra scripts in the load queue, and no dependencies to keep updated. A visual builder with dozens of ready-made styles is needed less often than it seems, but if you truly need one, look at MaxButtons in the wordpress.org directory.

Practical tip: start the implementation with the line color-scheme: light dark in :root and variables via light-dark(). This is the cheapest way to get a proper dark theme, and it pays off for every subsequent UI element. And a pitfall to finish: do not test the button only with a mouse. Tab through the page and verify that the focus ring is visible; otherwise some visitors simply will not find your button.

Grab the code, paste it into your child theme, and test the button on your site today. Did it work, or did you hit a conflict with your theme? Share in the comments, and we will figure it out together. And subscribe for blog updates: more useful shortcode breakdowns are coming.