← Back to notes
snippet 1 min read PHP 7.4+ WP 6.0+ WC 8.0+

WooCommerce's default variation dropdowns into selectable radio buttons

Replace WooCommerce variation dropdowns with selectable radio-button pills while keeping the native selects and variation logic intact.

Updated Jun 1, 2026 #woocommerce #wordpress #php #javascript #snippet

To convert WooCommerce’s default variation dropdowns into selectable radio buttons or pills, keep the native selects in the form and generate a custom visual control beside them.

This approach lets WooCommerce’s core JavaScript continue calculating prices, checking stock, and handling the Add to Cart flow. The custom pills become a presentation layer rather than a replacement for WooCommerce’s variation system.

Paste the complete solution into a child theme’s functions.php file, or add it through a code snippet plugin such as WPCode.

The complete code solution

functions.php
<?php
/**
* 1. Generate the HTML for the Radio/Pill Buttons
* This filters the default dropdown HTML, hides it, and adds our custom radio buttons.
*/
add_filter( 'woocommerce_dropdown_variation_attribute_options_html', 'convert_wc_dropdown_to_pills', 10, 2 );
function convert_wc_dropdown_to_pills( $html, $args ) {
$options = $args['options'];
$product = $args['product'];
$attribute = $args['attribute'];
// Fallback name if missing
$name = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute );
if ( empty( $options ) && ! empty( $product ) && ! empty( $attribute ) ) {
$attributes = $product->get_variation_attributes();
$options = $attributes[ $attribute ];
}
$radios = '<div class="wc-variation-pills">';
if ( ! empty( $options ) ) {
// Handle global attributes (taxonomies)
if ( $product && taxonomy_exists( $attribute ) ) {
$terms = wc_get_product_terms( $product->get_id(), $attribute, array( 'fields' => 'all' ) );
foreach ( $terms as $term ) {
if ( in_array( $term->slug, $options, true ) ) {
$checked = ( sanitize_title( $args['selected'] ) === $term->slug ) ? 'checked="checked"' : '';
$label = esc_html( apply_filters( 'woocommerce_variation_option_name', $term->name, $term, $attribute, $product ) );
$radios .= '<label class="wc-pill-label">';
$radios .= '<input type="radio" name="' . esc_attr( $name ) . '_pill" value="' . esc_attr( $term->slug ) . '" ' . $checked . '>';
$radios .= '<span>' . $label . '</span>';
$radios .= '</label>';
}
}
}
// Handle custom product attributes
else {
foreach ( $options as $option ) {
$checked = ( sanitize_title( $args['selected'] ) === sanitize_title( $option ) ) ? 'checked="checked"' : '';
$label = esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute, $product ) );
$radios .= '<label class="wc-pill-label">';
$radios .= '<input type="radio" name="' . esc_attr( $name ) . '_pill" value="' . esc_attr( $option ) . '" ' . $checked . '>';
$radios .= '<span>' . $label . '</span>';
$radios .= '</label>';
}
}
}
$radios .= '</div>';
// Wrap the original select in a hidden div, then append the new pills.
return '<div class="wc-dropdown-hidden" style="display:none;">' . $html . '</div>' . $radios;
}
/**
* 2. Sync the Pills with the Hidden Dropdown via JavaScript
* When a pill is clicked, it updates the hidden select so WooCommerce knows what was chosen.
*/
add_action( 'wp_footer', 'wc_variation_pills_script' );
function wc_variation_pills_script() {
// Only load on single product pages.
if ( ! is_product() ) {
return;
}
?>
<script type="text/javascript">
jQuery(document).ready(function($) {
$(document).on('change', '.wc-variation-pills input[type="radio"]', function() {
var $radio = $(this);
var value = $radio.val();
var name = $radio.attr('name').replace('_pill', '');
var $select = $radio.closest('td.value, div.value').find('select[name="' + name + '"]');
// Update the hidden dropdown and trigger WooCommerce's change event.
$select.val(value).trigger('change');
});
// Clear the visual selection when WooCommerce resets the variation form.
$('.variations_form').on('reset_data', function() {
$(this).find('.wc-variation-pills input[type="radio"]').prop('checked', false);
});
});
</script>
<?php
}
/**
* 3. Style the Radio Buttons to Look Like Pills
* Adds CSS to the header to hide the native radio circle and style the label.
*/
add_action( 'wp_head', 'wc_variation_pills_css' );
function wc_variation_pills_css() {
if ( ! is_product() ) {
return;
}
?>
<style>
.wc-variation-pills {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
}
.wc-pill-label {
margin: 0 !important;
cursor: pointer;
}
.wc-pill-label input[type="radio"] {
display: none !important;
}
.wc-pill-label span {
display: inline-block;
padding: 8px 18px;
border: 2px solid #e0e0e0;
border-radius: 50px;
background-color: #ffffff;
color: #333333;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease-in-out;
user-select: none;
}
.wc-pill-label:hover span {
border-color: #999999;
}
.wc-pill-label input[type="radio"]:checked + span {
border-color: #222222;
background-color: #222222;
color: #ffffff;
}
table.variations td {
padding-bottom: 15px;
}
</style>
<?php
}

How it works

  1. The PHP block loops through global and custom variation attributes and builds radio inputs alongside the original select. The select is wrapped in a hidden div rather than removed.
  2. The JavaScript block bridges the visual pills and the hidden select. When a pill changes, it updates the select and triggers WooCommerce’s native change event.
  3. The CSS block hides the native radio circle and styles each label as a clickable pill. Change #222222 and #e0e0e0 to match your store’s palette.

Why keep the native dropdown?

WooCommerce’s variation form expects its own select elements. Keeping them in the DOM means the existing variation matching, price updates, stock checks, and Add to Cart behavior remain available to the store.

The pills only improve the interface. They do not duplicate the product-variation logic or introduce a second source of truth.