Plugin

rankchoice.js

Drop-in replacement for <select multiple> that captures both the chosen values and the position the user puts them in. Tap to add, drag to reorder — the form posts an ordered array, no submit handler needed.

npm install @goboldlyforward/rankchoice

Try it

A real form, a real ranking

Tap to add a food. Each pick rises to the top and is numbered in the order you choose it. Drag the ranked items to reorder. Submit to see exactly what the server would receive.

Form payload


    

Install & usage

Drop it in

One stylesheet, one script, one attribute. No build step. Plugin auto-mounts on DOMContentLoaded.

<link rel="stylesheet" href="path/to/rankchoice.css">
<script src="path/to/rankchoice.js"></script>

<form action="/preferences" method="post">
  <select multiple data-rankchoice name="favorites">
    <option value="pizza">Pizza</option>
    <option value="tacos">Tacos</option>
    <option value="sushi">Sushi</option>
  </select>
  <button>Save</button>
</form>

The plugin hides the original <select> and writes a set of ordered hidden <input name="favorites[]"> fields. Rails reads params[:favorites] as ["tacos", "pizza"] — position is simply the array index. If JavaScript fails to load, the native multi-select still works as a fallback (just without ordering).

Markup

Attributes

All progressive enhancement — start from a working <select multiple>, then sprinkle data attributes for the extra features.

<!-- Required: a name and at least one option -->
<select multiple data-rankchoice name="favorites">
  <option value="pizza">Pizza</option>
</select>

<!-- Pre-select with order (overrides per-option `selected`) -->
<select multiple data-rankchoice name="favorites"
        data-value="tacos,pizza">
  <option value="pizza">Pizza</option>
  <option value="tacos">Tacos</option>
</select>

<!-- Per-option emoji and an accessible label -->
<select multiple data-rankchoice name="favorites"
        data-label="Rank your favorite foods">
  <option value="pizza" data-emoji="🍕">Pizza</option>
  <option value="tacos" data-emoji="🌮">Tacos</option>
</select>

JavaScript API

For when auto-init isn't enough

// Manual init (after injecting new selects into the DOM)
RankChoice.initAll(scope);          // scan a subtree for [data-rankchoice]
RankChoice.init(selectElement);     // mount one

// Get the live instance for a select
const inst = RankChoice.instances.get(selectElement);
inst.getValue();                    // ["tacos", "pizza"]
inst.setValue(["sushi", "pizza"]);  // replace the selection (animated)
inst.destroy();                     // restore the original <select>

// Listen for changes — fired on the original <select> (both bubble)
selectElement.addEventListener("change", (e) => {
  // native-style; query the select or instance for the value
});
selectElement.addEventListener("rankchoice:change", (e) => {
  console.log(e.detail.value);      // ["tacos", "pizza"]
});

On every change the original <option selected> attributes are kept in sync (in option order — native HTML has no concept of selection order) and the hidden submission inputs are rewritten in ranked order, so the form posts the right thing whether you submit normally or build your own FormData.