To convert WooCommerce’s default variation dropdowns into selectable radio buttons or “pills,” the most reliable method is to keep the default dropdowns hidden and generate custom radio buttons.
This ensures that WooCommerce’s core JavaScript (which calculates prices, checks stock, and handles the “Add to Cart” button) continues to work perfectly in the background without breaking your site.
Here is the complete, self-contained code. You can paste this directly into your child theme’s functions.php file, or use a code snippet plugin like WPCode.
The Complete Code Solution
<?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 original select in a hidden div, append the new pills
$html = '<div class="wc-dropdown-hidden" style="display:none;">' . $html . '</div>' . $radios;
return $html;
}
/**
* 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($) {
// Trigger when a pill is clicked
$(document).on('change', '.wc-variation-pills input[type="radio"]', function() {
var $radio = $(this);
var value = $radio.val();
// Get the name of the original select field
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');
});
// If the user clicks the "Clear" button, uncheck our pills
$('.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 actual radio circle and style the span tag.
*/
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;
}
/* Hide the native radio button entirely */
.wc-pill-label input[type="radio"] {
display: none !important;
}
/* The Pill Design */
.wc-pill-label span {
display: inline-block;
padding: 8px 18px;
border: 2px solid #e0e0e0;
border-radius: 50px; /* Makes it a pill shape. Change to 4px for square buttons */
background-color: #ffffff;
color: #333333;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease-in-out;
user-select: none;
}
/* Hover state */
.wc-pill-label:hover span {
border-color: #999999;
}
/* Selected (Checked) state */
.wc-pill-label input[type="radio"]:checked + span {
border-color: #222222;
background-color: #222222;
color: #ffffff;
}
/* Clear WooCommerce default margin on td */
table.variations td {
padding-bottom: 15px;
}
</style>
<?php
}
How this works:
- The PHP Block loops through your variation attributes (both global and custom) and builds standard HTML
<input type="radio">elements alongside the original<select>tag. It wraps the<select>in a hiddendiv. - The JavaScript Block acts as the bridge. Whenever a user clicks your new pill, it finds the exact value they selected and silently passes it to the hidden WooCommerce dropdown. This perfectly preserves your theme’s Add-to-Cart logic.
- The CSS Block hides the default tiny radio circles and applies padding, borders, and colors to make them look like modern, clickable “pills.” You can easily tweak the hex codes (
#222222,#e0e0e0) in the CSS to perfectly match your brand colors.