Answering my own question as I think this is useful. Adrian at Expresso helped me out.
Basically, we output the menus and also output custom buttons. Then we just hide the menus with CSS. This presumably avoids writing lots of new custom Javascript or introducing new dependencies to the way the form is processed by Store.
See this jsFiddle
EE code
{!-- EE/Store code for the <select>s that actually control what gets added to the Cart: --}
{modifiers}
<select id="{modifier_input_name}" name="{modifier_input_name}">
{modifier_options}
<option value="{option_id}">
{option_name} {if price_mod_val} ({price_mod}) {/if}
</option>
{/modifier_options}
</select>
{/modifiers}
<!-- EE/Store code for radio buttons:-->
{modifiers}
<ul class="radio-set">
{modifier_options}
<li><a class="radio" data-select="{modifier_input_name}" data-option-val="{option_id}" href="#">{option_name} {if price_mod_val} ({price_mod}) {/if}</a></li>
{/modifier_options}
</ul>
{/modifiers}
jQuery Code
$(document).ready(function() {
// highlight the first button, since it corresponds to the (default) <option> in the <select>
$("ul.radio-set li:first-child a.radio").addClass("active");
// button clicks: we only need the buttons to change the <select>, not the other way around (since the select will be hidden by CSS
$('a.radio').click(function() {
var this_button = $(this);
var this_ul = this_button.closest("ul.radio-set");
var neighbor_buttons = this_ul.find('a.radio');
// remove highlights from all buttons in this group
$(neighbor_buttons).removeClass("active");
// highlight this buton
this_button.addClass("active");
// get the corresponding select menu
var select_id = $(this_button).data("select");
var select = "#"+select_id;
// get the value of the corresponding option
var opt_val = $(this_button).data("option-val");
// Update the select menu with the option content and trigger change() for Store
$(select).val(opt_val).trigger('change');
return false;
});
});