Monthly / yearly pricing toggle shortcode
A pricing table shortcode with an accessible monthly and yearly switch driven by data attributes and a few lines of script.
WordPressPricingpricingshortcodetoggleswitch
Live preview
Code
<?php
/**
* [acme_pricing] shortcode.
*
* @package Acme_Theme
*/
defined( 'ABSPATH' ) || exit;
function acme_pricing_shortcode(): string {
$plans = array(
array( 'name' => 'Starter', 'month' => 12, 'year' => 120, 'features' => array( '1 project', 'Email support' ) ),
array( 'name' => 'Pro', 'month' => 29, 'year' => 290, 'features' => array( '10 projects', 'Priority support', 'Team seats' ), 'featured' => true ),
);
wp_enqueue_script( 'acme-pricing', get_template_directory_uri() . '/assets/pricing.js', array(), '1.0.0', array( 'in_footer' => true, 'strategy' => 'defer' ) );
$out = '<div class="acme-pricing" data-period="month">';
$out .= '<div class="acme-pricing__switch" role="group" aria-label="' . esc_attr__( 'Billing period', 'acme-theme' ) . '">';
$out .= '<button type="button" data-set="month" aria-pressed="true">' . esc_html__( 'Monthly', 'acme-theme' ) . '</button>';
$out .= '<button type="button" data-set="year" aria-pressed="false">' . esc_html__( 'Yearly', 'acme-theme' ) . '</button></div>';
$out .= '<div class="acme-pricing__grid">';
foreach ( $plans as $plan ) {
$out .= '<article class="acme-plan' . ( ! empty( $plan['featured'] ) ? ' is-featured' : '' ) . '">';
$out .= '<h3>' . esc_html( $plan['name'] ) . '</h3>';
$out .= sprintf(
'<p class="acme-plan__price"><span data-price-month>$%1$d</span><span data-price-year hidden>$%2$d</span> <small data-unit>%3$s</small></p>',
(int) $plan['month'],
(int) $plan['year'],
esc_html__( 'per month', 'acme-theme' )
);
$out .= '<ul>';
foreach ( $plan['features'] as $feature ) {
$out .= '<li>' . esc_html( $feature ) . '</li>';
}
$out .= '</ul></article>';
}
return $out . '</div></div>';
}
add_shortcode( 'acme_pricing', 'acme_pricing_shortcode' );
document.querySelectorAll('.acme-pricing').forEach((root) => {
const units = { month: 'per month', year: 'per year' };
root.querySelectorAll('[data-set]').forEach((btn) => btn.addEventListener('click', () => {
const period = btn.dataset.set;
root.dataset.period = period;
root.querySelectorAll('[data-set]').forEach((b) => b.setAttribute('aria-pressed', String(b === btn)));
root.querySelectorAll('[data-price-month]').forEach((el) => { el.hidden = period !== 'month'; });
root.querySelectorAll('[data-price-year]').forEach((el) => { el.hidden = period !== 'year'; });
root.querySelectorAll('[data-unit]').forEach((el) => { el.textContent = units[period]; });
}));
});
How it works
What it solves
SaaS price tables usually offer a cheaper yearly option, and a switch keeps the page compact. The shortcode renders every price into the HTML, so search engines and non-JavaScript readers see real numbers, then a small script flips which one is visible.
How it works
Plans live in a PHP array, so changing prices is a code or option edit rather than page markup. Both the monthly and yearly figures are printed, with the yearly ones hidden. Two buttons form a labelled group and use aria-pressed to expose their state. On click the script sets the period on the wrapper, hides and reveals the matching spans, and swaps the unit text.
Only integers and escaped strings reach the output.
Adapting it
Load the plans from an options page or custom post type instead of the literal array. Add a savings badge by computing 1 - year / (month * 12). Style .is-featured and use [data-period] in CSS if you prefer to show and hide with selectors rather than the hidden attribute.
Pitfalls
- Translate unit labels in PHP and pass them as data attributes if the script must show them.
- Make buttons real buttons, not links, so Space and Enter work.
- Do not rely on colour alone to show the active period.
- Currency formatting differs by locale; use
number_format_i18n()for large figures.
How to use it
Save the PHP as inc/pricing.php, require it, and put the script in assets/.
Add [acme_pricing] to any page.
Change the $plans array or feed it from your own data source.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.