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.
goboldlyforward.github.io/rankchoice — tap to add, drag the ranked items to reorder, submit to see what the server would receive.
- Auto-mounts on any
<select multiple data-rankchoice>. Hides the native control and renders a tap-to-pick / drag-to-reorder UI in its place. - Each pick rises to the top of the ranked list and is numbered in the order chosen.
- Writes a set of ordered hidden
<input name="<name>[]">fields so the form posts an array — Rails readsparams[:favorites]as["tacos", "pizza"], position is the array index. - Original
<option selected>attributes are kept in sync on every change (in option order — native HTML has no concept of selection order), so server-side fallback still works if your backend doesn't use the[]form. - Progressive enhancement: if JavaScript fails to load, the native multi-select still works (just without ordering).
- Optional per-option emoji via
data-emoji.
No framework, no build step. Drop in the CSS + JS and add an attribute.
npm install @goboldlyforward/rankchoiceOr grab the files directly:
<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" data-emoji="🍕">Pizza</option>
<option value="tacos" data-emoji="🌮">Tacos</option>
<option value="sushi" data-emoji="🍣">Sushi</option>
</select>
<button>Save</button>
</form>The plugin auto-inits on DOMContentLoaded.
| Attribute | On | What it does |
|---|---|---|
data-rankchoice |
<select> |
Required. Marks the element for auto-init. |
name |
<select> |
Required. Used for the hidden name[] inputs. |
data-value |
<select> |
Comma-separated ids in position order. Overrides per-option selected. |
data-label |
<select> |
Accessible label for the widget. |
data-emoji |
<option> |
Emoji shown alongside the label. |
// Manual init (after injecting new selects)
RankChoice.initAll(scope); // scan a subtree for [data-rankchoice]
RankChoice.init(selectElement); // mount one
// Get the live instance
const inst = RankChoice.instances.get(selectElement);
inst.getValue(); // ["tacos", "pizza"]
inst.setValue(["sushi", "pizza"]); // replace selection (animated)
inst.destroy(); // restore the original <select>Both fire on the original <select> and 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"]
});HTML, CSS, and ~20KB of JavaScript. No framework, no build step. Native HTML5 drag-and-drop.
MIT — see LICENSE.