diff --git a/core/static/bundled/alpine-index.js b/core/static/bundled/alpine-index.js
index 211600a5..4e04e7dd 100644
--- a/core/static/bundled/alpine-index.js
+++ b/core/static/bundled/alpine-index.js
@@ -1,7 +1,8 @@
+import { limitedChoices } from "#core:alpine/limited-choices";
 import sort from "@alpinejs/sort";
 import Alpine from "alpinejs";
 
-Alpine.plugin(sort);
+Alpine.plugin([sort, limitedChoices]);
 window.Alpine = Alpine;
 
 window.addEventListener("DOMContentLoaded", () => {
diff --git a/core/static/bundled/alpine/limited-choices.ts b/core/static/bundled/alpine/limited-choices.ts
new file mode 100644
index 00000000..211441d0
--- /dev/null
+++ b/core/static/bundled/alpine/limited-choices.ts
@@ -0,0 +1,69 @@
+import type { Alpine as AlpineType } from "alpinejs";
+
+export function limitedChoices(Alpine: AlpineType) {
+  /**
+   * Directive to limit the number of elements
+   * that can be selected in a group of checkboxes.
+   *
+   * When the max numbers of selectable elements is reached,
+   * new elements will still be inserted, but oldest ones will be deselected.
+   * For example, if checkboxes A, B and C have been selected and the max
+   * number of selections is 3, then selecting D will result in having
+   * B, C and D selected.
+   *
+   * # Example in template
+   * ```html
+   * <div x-data="{nbMax: 2}", x-limited-choices="nbMax">
+   *   <button @click="nbMax += 1">Click me to increase the limit</button>
+   *   <input type="checkbox" value="A" name="foo">
+   *   <input type="checkbox" value="B" name="foo">
+   *   <input type="checkbox" value="C" name="foo">
+   *   <input type="checkbox" value="D" name="foo">
+   * </div>
+   * ```
+   */
+  Alpine.directive(
+    "limited-choices",
+    (el, { expression }, { evaluateLater, effect }) => {
+      const getMaxChoices = evaluateLater(expression);
+      let maxChoices: number;
+      const inputs: HTMLInputElement[] = Array.from(
+        el.querySelectorAll("input[type='checkbox']"),
+      );
+      const checked = [] as HTMLInputElement[];
+
+      const manageDequeue = () => {
+        if (checked.length <= maxChoices) {
+          // There isn't too many checkboxes selected. Nothing to do
+          return;
+        }
+        const popped = checked.splice(0, checked.length - maxChoices);
+        for (const p of popped) {
+          p.checked = false;
+        }
+      };
+
+      for (const input of inputs) {
+        input.addEventListener("change", (_e) => {
+          if (input.checked) {
+            checked.push(input);
+          } else {
+            checked.splice(checked.indexOf(input), 1);
+          }
+          manageDequeue();
+        });
+      }
+      effect(() => {
+        getMaxChoices((value: string) => {
+          const previousValue = maxChoices;
+          maxChoices = Number.parseInt(value);
+          if (maxChoices < previousValue) {
+            // The maximum number of selectable items has been lowered.
+            // Some currently selected elements may need to be removed
+            manageDequeue();
+          }
+        });
+      });
+    },
+  );
+}
diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja
index 28b2a956..2c628211 100644
--- a/election/templates/election/election_detail.jinja
+++ b/election/templates/election/election_detail.jinja
@@ -63,7 +63,13 @@
         {%- for role in role_list %}
           {%- set count = [0] %}
           {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %}
-          <tbody data-max-choice="{{role.max_choice}}" class="role{{ ' role_error' if role.title in election_form.errors else '' }}{{ ' role__multiple-choices' if role.max_choice > 1 else ''}}">
+
+          <tbody
+            {% if role.max_choice > 1 -%}
+              x-data x-limited-choices="{{ role.max_choice }}"
+            {%- endif %}
+            class="role {% if role.title in election_form.errors %}role_error{% endif %}"
+          >
             <tr>
               <td class="role_title">
                 <div class="role_text">
@@ -197,30 +203,3 @@
     </section>
   {%- endif %}
 {% endblock %}
-
-{% block script %}
-  {{ super() }}
-  <script type="text/javascript">
-    document.querySelectorAll('.role__multiple-choices').forEach(setupRestrictions);
-
-    function setupRestrictions(role) {
-      var selectedChoices = [];
-      role.querySelectorAll('input').forEach(setupRestriction);
-
-      function setupRestriction(choice) {
-        if (choice.checked)
-          selectedChoices.push(choice);
-        choice.addEventListener('change', onChange);
-
-        function onChange() {
-          if (choice.checked)
-            selectedChoices.push(choice);
-          else
-            selectedChoices.splice(selectedChoices.indexOf(choice), 1);
-          while (selectedChoices.length > role.dataset.maxChoice)
-            selectedChoices.shift().checked = false;
-        }
-      }
-    }
-  </script>
-{% endblock %}