diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py
index b4e6c32a5a9709c5753341ea3275d64b8ab82c63..ba15e0fca4ed10be2b4a3e37895dbd78866c4527 100644
--- a/app/scodoc/sco_groups_view.py
+++ b/app/scodoc/sco_groups_view.py
@@ -56,12 +56,15 @@ from app.scodoc.gen_tables import GenTable
 from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
 from app.scodoc.sco_permissions import Permission
 
-JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
+JAVASCRIPTS = html_sco_header.BOOTSTRAP_JS + [
     "js/etud_info.js",
     "js/groups_view.js",
+    "js/multi-select.js",
 ]
 
-CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
+CSSSTYLES = html_sco_header.BOOTSTRAP_CSS + [
+    "libjs/bootstrap/css/bootstrap.min.css",
+]
 
 
 # view:
@@ -115,13 +118,10 @@ def groups_view(
     return f"""
     { html_sco_header.sco_header(
             javascripts=JAVASCRIPTS,
-            cssstyles=CSSSTYLES,
+            cssstyles=CSSSTYLES
         )
     }
     <style>
-    div.multiselect-container.dropdown-menu {{
-        min-width: 180px;
-    }}
     span.warning_unauthorized {{
         color: pink;
         font-style: italic;
@@ -221,47 +221,70 @@ def form_groups_choice(
 
 
 def menu_groups_choice(
-    groups_infos, submit_on_change=False, default_deselect_others=True
+    groups_infos,
+    submit_on_change=False,
+    default_deselect_others=True,
+    html_export=True,
+    change_event=None,
 ):
     """menu pour selection groupes
     group_ids est la liste des groupes actuellement sélectionnés
     et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
     (utilisé pour retrouver le semestre et proposer la liste des autres groupes)
+
+    Si html_export :
+        selecteur.value = &group_ids=xxx&group_ids=yyy...
+    sinon :
+        selecteur.value = [xxx, yyy, ...]
+
+    Si change_event :
+        met à jour l'événement onchange du selecteur
+        (attend du js, plus d'informations sur scu.MultiSelect.change_event)
+
     """
     default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
     n_members = len(sco_groups.get_group_members(default_group_id))
 
-    H = [
-        f"""<select name="group_ids" id="group_ids_sel"
-            class="multiselect
-                {'submit_on_change' if submit_on_change else ''}
-                {'default_deselect_others' if default_deselect_others else ''}
-                "
-            multiple="multiple">
-                <option class="default_group"
-                    value="{default_group_id}"
-                    {'selected' if default_group_id in groups_infos.group_ids else ''}
-                >Tous ({n_members})</option>
-        """
-    ]
+    values: dict = {
+        # Choix : Tous (tous les groupes)
+        "": [
+            {
+                "value": default_group_id,
+                "label": f"Tous ({n_members})",
+                "selected": default_group_id in groups_infos.group_ids,
+                "single": default_deselect_others,
+            }
+        ]
+    }
 
     for partition in groups_infos.partitions:
-        H.append('<optgroup label="%s">' % partition["partition_name"])
+        p_name: str = partition["partition_name"]
+        vals: list[tuple[str, str, bool]] = []
         # Les groupes dans cette partition:
-        for g in sco_groups.get_partition_groups(partition):
-            if g["group_id"] in groups_infos.group_ids:
-                selected = "selected"
-            else:
-                selected = ""
-            if g["group_name"]:
-                n_members = len(sco_groups.get_group_members(g["group_id"]))
-                H.append(
-                    '<option value="%s" %s>%s (%s)</option>'
-                    % (g["group_id"], selected, g["group_name"], n_members)
+        for grp in sco_groups.get_partition_groups(partition):
+            selected: bool = grp["group_id"] in groups_infos.group_ids
+            if grp["group_name"]:
+                vals.append(
+                    {
+                        "value": grp["group_id"],
+                        "label": f"{grp['group_name']} ({len(sco_groups.get_group_members(grp['group_id']))})",
+                        "selected": selected,
+                    }
                 )
-        H.append("</optgroup>")
-    H.append("</select> ")
-    return "\n".join(H)
+
+        values[p_name] = vals
+
+    multi_select: scu.MultiSelect = scu.MultiSelect(
+        values=values, name="group_ids", html_id="group_ids_sel"
+    )
+
+    if html_export:
+        multi_select.export_format('return "&group_ids="+values.join("&group_ids=")')
+
+    if submit_on_change:
+        multi_select.change_event("submit_group_selector();")
+
+    return multi_select.html()
 
 
 def menu_group_choice(group_id=None, formsemestre_id=None):
@@ -698,7 +721,6 @@ def groups_table(
             """
         ]
         if groups_infos.members:
-            menu_options = []
             options = {
                 "with_codes": "Affiche codes",
             }
@@ -711,34 +733,33 @@ def groups_table(
                         "with_bourse": "Statut boursier",
                     }
                 )
+            valeurs: list[tuple[str, str]] = []
             for option, label in options.items():
-                if locals().get(option, False):
-                    selected = "selected"
-                else:
-                    selected = ""
-                menu_options.append(
-                    f"""<option value="{option}" {selected}>{label}</option>"""
+                selected = locals().get(option, False)
+                valeurs.append(
+                    {
+                        "value": option,
+                        "label": label,
+                        "selected": selected,
+                    }
                 )
 
+            multi_select: scu.MultiSelect = scu.MultiSelect(
+                values={"": valeurs},
+                label="Options",
+                name="options",
+                html_id="group_list_options",
+            )
+            multi_select.change_event("change_list_options(values)")
             H.extend(
+                # ;
                 [
-                    """<span style="margin-left: 2em;">
-                    <select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
-                    "\n".join(menu_options),
-                    """</select></span>
-                    <script type="text/javascript">
-                    $(document).ready(function() {
-                    $('#group_list_options').multiselect(
-                    {
-                    includeSelectAllOption: false,
-                    nonSelectedText:'Options...',
-                    onChange: function(element, checked){
-                        change_list_options();
-                    }
-                    }
-                    );
-                    });
-                    </script>
+                    f"""
+                    <span style="margin-left: 2em;">
+
+                    {multi_select.html()}
+
+                    </span>
                     """,
                     (
                         """<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
@@ -929,11 +950,14 @@ def tab_absences_html(groups_infos, etat=None):
         """
         ]
 
-    url_feuille_appel: str = url_for(
-        "scolar.formulaire_feuille_appel",
-        scodoc_dept=g.scodoc_dept,
-        formsemestre_id=groups_infos.formsemestre_id,
-        group_ids=group_ids,
+    url_feuille_appel: str = (
+        url_for(
+            "scolar.formulaire_feuille_appel",
+            scodoc_dept=g.scodoc_dept,
+            formsemestre_id=groups_infos.formsemestre_id,
+        )
+        + "&"
+        + groups_infos.groups_query_args
     )
 
     H.extend(
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index 29bccd68af4f0704a87f07fef591cfdf6a1b735d..a97834cb0ec2032daf134f90d3d9e1a0e05770ba 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -26,8 +26,8 @@
 ##############################################################################
 
 
-""" Common definitions
-"""
+"""Common definitions"""
+
 import base64
 import bisect
 import collections
@@ -445,6 +445,121 @@ def translate_assiduites_metric(metric, inverse=True, short=True) -> str:
         return None
 
 
+class MultiSelect:
+    """
+    Classe pour faciliter l'utilisation du multi-select HTML/JS
+
+    Les values sont représentées en dict {
+        value: "...",
+        label:"...",
+        selected: True/False (default to False),
+        single: True/False (default to False)
+    }
+
+    Args:
+        values (dict[str, list[dict]]): Dictionnaire des valeurs
+            génère des <optgroup> pour chaque clef du dictionnaire
+            génère des <option> pour chaque valeur du dictionnaire
+        name (str, optional): Nom du multi-select. Defaults to "multi-select".
+        html_id (str, optional): Id HTML du multi-select. Defaults to "multi-select".
+        classname (str, optional): Classe CSS du multi-select. Defaults to "".
+        label (str, optional): Label du multi-select. Defaults to "".
+        export (str, optional): Format du multi-select (HTML/JS). Defaults to "js".
+            HTML : group_ids="val1"&group_ids="val2"...
+            JS : ["val1","val2", ...]
+
+        **kwargs: Arguments supplémentaires (appliqué au multiselect en HTML <multi-select key="value" ...>)
+    """
+
+    def __init__(
+        self,
+        values: dict[str, list[dict]],
+        name="multi-select",
+        html_id="multi-select",
+        label="",
+        classname="",
+        **kwargs,
+    ) -> None:
+        self.values: dict[str, list[dict]] = values
+        self._on = ""
+
+        self.name: str = name
+        self.html_id: str = html_id
+        self.classname: str = classname
+        self.label: str = label or name
+
+        self.args: dict = kwargs
+        self.js: str = ""
+        self.export: str = "return values"
+
+    def html(self) -> str:
+        """
+        Génère l'HTML correspondant au multi-select
+        """
+        opts: list[str] = []
+
+        for key, values in self.values.items():
+            optgroup = f"<optgroup label='{key}'>"
+            for value in values:
+                selected = "selected" if value.get("selected", False) else ""
+                single = "single" if value.get("single", False) else ""
+                opt = f"<option value='{value.get('value')}' {selected} {single} >{value.get('label')}</option>"
+                optgroup += opt
+            optgroup += "</optgroup>"
+            opts.append(optgroup)
+
+        args: list[str] = [f'{key}="{value}"' for key, value in self.args.items()]
+        js: str = "{" + self.js + "}"
+        export: str = "{" + self.export + "}"
+        return f"""
+        <multi-select 
+            label="{self.label}"
+            id="{self.html_id}"
+            name="{self.name}"
+            class="{self.classname}"
+            {" ".join(args)}
+        >
+            {"".join(opts)}
+        </multi-select>
+        <script>
+            window.addEventListener('load', () => {{document.getElementById("{self.html_id}").on((values)=>{js});
+            document.getElementById("{self.html_id}").format((values)=>{export});}} );
+        </script>
+        """
+
+    def change_event(self, js: str) -> None:
+        """
+        Met à jour l'évènement de changement de valeur du multi-select
+
+        CallBack JS : (values) => {/*actions à effectuer*/}
+
+        Sera retranscrit dans l'HTML comme :
+
+        document.getElementById(%self.id%).on((values)=>{%self.js%})
+
+        Exemple d'utilisation :
+
+        js : "console.log(values)"
+        """
+        self.js: str = js
+
+    def export_format(self, js: str) -> None:
+        """
+        Met à jour le format de retour de valeur du multi-select
+
+        CallBack JS : (values) => {/*actions à effectuer*/}
+
+        Sera retranscrit dans l'HTML comme :
+
+        document.getElementById(%self.id%).format((values)=>{%self.js%})
+
+        Exemple d'utilisation :
+
+        js : "return values.map(v=> 'val:'+v)"
+        """
+        self.export: str = js
+
+
 # Types de modules
 class ModuleType(IntEnum):
     """Code des types de module."""
diff --git a/app/static/js/groups_view.js b/app/static/js/groups_view.js
index 7be2509fa0bb9fcdcd774985d7e834c93573d250..29517a325d7b57ba8dc6be2ada0b7fdc163e87f9 100644
--- a/app/static/js/groups_view.js
+++ b/app/static/js/groups_view.js
@@ -13,72 +13,41 @@ $().ready(function () {
 // (ne fonctionne que pour les requetes GET: manipule la query string)
 
 function groups_view_url() {
-  var url = $.url();
-  delete url.param()["group_ids"]; // retire anciens groupes de l'URL
-  delete url.param()["curtab"]; // retire ancien tab actif
-  if (CURRENT_TAB_HASH) {
-    url.param()["curtab"] = CURRENT_TAB_HASH;
-  }
-  delete url.param()["formsemestre_id"];
-  url.param()["formsemestre_id"] =
-    $("#group_selector")[0].formsemestre_id.value;
-
-  var selected_groups = $("#group_selector select#group_ids_sel").val();
-  url.param()["group_ids"] = selected_groups; // remplace par groupes selectionnes
-
-  return url;
-}
-
-// Sélectionne le groupe "tous" et recharge la page:
-function select_groupe_tous() {
-  var url = groups_view_url();
-  var default_group_id = $("#group_selector")[0].default_group_id.value;
-  delete url.param()["group_ids"];
-  url.param()["group_ids"] = [default_group_id];
-
-  var query_string = $.param(url.param(), (traditional = true));
-  window.location = url.attr("base") + url.attr("path") + "?" + query_string;
+  let url = new URL(location.href);
+  let urlParams = url.searchParams;
+  // ajout du formsemestre
+  urlParams.set(
+    "formsemestre_id",
+    $("#group_selector")[0].formsemestre_id.value
+  );
+  // ajout du tab actif
+  const tabActif = document.querySelector(
+    '[role="tab"][aria-selected="true"]'
+  ).id;
+  urlParams.set("tab", tabActif);
+  urlParams.delete("group_ids");
+  // ajout des groupes selectionnes
+  var selected_groups = document.getElementById("group_ids_sel").value;
+  url.search = urlParams.toString() + selected_groups;
+  return url.href;
 }
 
 // Recharge la page sans arguments group_ids
 function remove_group_filter() {
-  var url = groups_view_url();
-  delete url.param()["group_ids"];
-  var query_string = $.param(url.param(), (traditional = true));
-  window.location = url.attr("base") + url.attr("path") + "?" + query_string;
-}
+  var url = new URL(location.href);
+  url.searchParams.delete("group_ids");
 
-// L'URL pour l'état courant de la page:
-function get_current_url() {
-  var url = groups_view_url();
-  var query_string = $.param(url.param(), (traditional = true));
-  return url.attr("base") + url.attr("path") + "?" + query_string;
+  window.location = url.href;
 }
 
 // Recharge la page en changeant les groupes selectionnés et en conservant le tab actif:
 function submit_group_selector() {
-  window.location = get_current_url();
+  window.location = groups_view_url();
 }
 
-function show_current_tab() {
-  if (document.getElementsByClassName("nav-tabs").length > 0) {
-    $('.nav-tabs [href="#' + CURRENT_TAB_HASH + '"]').tab("show");
-  }
-}
-
-var CURRENT_TAB_HASH = $.url().param()["curtab"];
-
-$().ready(function () {
-  $(".nav-tabs a").on("shown.bs.tab", function (e) {
-    CURRENT_TAB_HASH = e.target.hash.slice(1); // sans le #
-  });
-
-  show_current_tab();
-});
-
-function change_list_options() {
-  var url = groups_view_url();
-  var selected_options = $("#group_list_options").val();
+function change_list_options(selected_options) {
+  var url = new URL(groups_view_url());
+  var urlParams = url.searchParams;
   var options = [
     "with_paiement",
     "with_archives",
@@ -88,13 +57,11 @@ function change_list_options() {
   ];
   for (var i = 0; i < options.length; i++) {
     var option = options[i];
-    delete url.param()[option];
     if ($.inArray(option, selected_options) >= 0) {
-      url.param()[option] = 1;
+      urlParams.set(option, "1");
     }
   }
-  var query_string = $.param(url.param(), (traditional = true));
-  window.location = url.attr("base") + url.attr("path") + "?" + query_string;
+  window.location = url.href;
 }
 
 // Menu choix groupe:
@@ -134,57 +101,6 @@ function toggle_visible_etuds() {
     .forEach((elem) => (elem.value = group_ids_str));
 }
 
-$().ready(function () {
-  $("#group_ids_sel").multiselect({
-    includeSelectAllOption: false,
-    nonSelectedText: "choisir...",
-    // buttonContainer: '<div id="group_ids_sel_container"/>',
-    onChange: function (element, checked) {
-      // Gestion du groupe "tous"
-      if (
-        checked == true &&
-        $("#group_ids_sel").hasClass("default_deselect_others")
-      ) {
-        var default_group_id = $(".default_group")[0].value;
-
-        if (element.hasClass("default_group")) {
-          // click sur groupe "tous"
-          // deselectionne les autres
-          $("#group_ids_sel option:selected").each(function (index, opt) {
-            if (opt.value != default_group_id) {
-              $("#group_ids_sel").multiselect("deselect", opt.value);
-            }
-          });
-        } else {
-          // click sur un autre item
-          // si le groupe "tous" est selectionne et que l'on coche un autre, le deselectionner
-          var default_is_selected = false;
-          $("#group_ids_sel option:selected").each(function (index, opt) {
-            if (opt.value == default_group_id) {
-              default_is_selected = true;
-              return false;
-            }
-          });
-          if (default_is_selected) {
-            $("#group_ids_sel").multiselect("deselect", default_group_id);
-          }
-        }
-      }
-
-      toggle_visible_etuds();
-      // referme le menu apres chaque choix:
-      $("#group_selector .btn-group").removeClass("open");
-
-      if ($("#group_ids_sel").hasClass("submit_on_change")) {
-        submit_group_selector();
-      }
-    },
-  });
-
-  // initial setup
-  toggle_visible_etuds();
-});
-
 // Trombinoscope
 $().ready(function () {
   var elems = $(".trombi-photo");
@@ -211,3 +127,11 @@ $().ready(function () {
     });
   }
 });
+
+// gestion du tab actif
+$().ready(function () {
+  let tab = new URL(location.href).searchParams.get("tab");
+  if (tab) {
+    document.getElementById(tab)?.click();
+  }
+});
diff --git a/app/static/js/multi-select.js b/app/static/js/multi-select.js
new file mode 100644
index 0000000000000000000000000000000000000000..e10194ad24cfe753ae7252b90f6a471827d04d46
--- /dev/null
+++ b/app/static/js/multi-select.js
@@ -0,0 +1,303 @@
+/* <== définition Multi-Select ==> */
+
+/**
+ * Permet d'afficher un sélecteur multiple d'options.
+ * Pour chaque option cela affichera un checkbox.
+ * Les options peuvent être regroupées dans des optgroup.
+ *
+ * 
+ * Utilisation : 
+ * <multi-select>
+        <optgroup label="Groupe A">
+            <option value="val1">Option 1</option>
+            <option value="val2">Option 2</option>
+        </optgroup>
+        <optgroup label="Groupe B">
+            <option value="valB1">Option B1</option>
+            <option value="valB2">Option B2</option>
+        </optgroup>
+    </multi-select>
+
+    <multi-select>.values() => ["val1",...]
+    <multi-select>.values(["val1",...]) => // sélectionne les options correspondantes (ne vérifie pas les options "single")
+    <multi-select>.on("change", (values) => {}) => // écoute le changement de valeur
+ */
+
+class MultiSelect extends HTMLElement {
+  static formAssociated = true;
+
+  get form() {
+    return this._internals.form;
+  }
+  get name() {
+    return this.getAttribute("name");
+  }
+
+  get label() {
+    return this.getAttribute("label");
+  }
+
+  set label(value) {
+    this.setAttribute("label", value);
+  }
+
+  get type() {
+    return this.localName;
+  }
+
+  constructor() {
+    super();
+    this.attachShadow({ mode: "open" });
+
+    // HTML/CSS du composant
+    this.shadowRoot.innerHTML = `
+                  <style>
+                    *{
+                      box-sizing: border-box;
+                    }
+                      
+                    .dropdown {
+                      position: relative;
+                      display: inline-block;
+                      border-radius: 10px;
+                      user-select: none;
+                    }
+                    .dropdown-button {
+                      // padding: 10px;
+                      // background-color: #f1f1f1;
+                      // border: 1px solid #ccc;
+                      cursor: pointer;
+                      font-family: inherit;
+                      font-size: inherit;
+                      line-height: inherit;
+                    }
+                    .dropdown-content {
+                      display: none;
+                      position: absolute;
+                      background-color: #fff;
+                      min-width: 200px;
+                      max-height: 200px;
+                      overflow-y: auto;
+                      box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
+                      z-index: 1;
+                    }
+                    .dropdown-content .optgroup {
+                      padding: 10px;
+                      width: 100%;
+                    }
+                    .dropdown-content .optgroup div {
+                      font-weight: bold;
+                    }
+                    .dropdown-content .option {
+                      display: flex;
+                      align-items: center;
+                    }
+                    .dropdown-content .option input[type="checkbox"] {
+                      margin-right: 0.5em;
+                    }
+                    label.selected{
+                      background-color: #C2DBFB;
+                    }
+                    label{
+                      cursor: pointer;
+                      transition: all 0.3s;
+                    }
+                    label:hover{
+                      background-color: #f1f1f1;
+                    }
+                  </style>
+                  <div class="dropdown">
+                    <button class="dropdown-button">Select options</button>
+                    <div class="dropdown-content multi-select-container"></div>
+                  </div>
+                `;
+
+    this.exportFormat = null;
+    this.observer = new MutationObserver(() => this.render());
+
+    this.toggleDropdown = this.toggleDropdown.bind(this);
+    this.handleDocumentClick = this.handleDocumentClick.bind(this);
+
+    this._internals = this.attachInternals();
+    this._internals.setFormValue([]);
+  }
+
+  connectedCallback() {
+    this.render();
+    this.observer.observe(this, { childList: true, subtree: true });
+    const btn = this.shadowRoot.querySelector(".dropdown-button");
+    btn.addEventListener("click", this.toggleDropdown);
+    document.addEventListener("click", this.handleDocumentClick);
+
+    this._updateSelect();
+  }
+
+  disconnectedCallback() {
+    this.observer.disconnect();
+    document.removeEventListener("click", this.handleDocumentClick);
+  }
+
+  toggleDropdown(event) {
+    event.stopPropagation();
+    const dropdownContent = this.shadowRoot.querySelector(".dropdown-content");
+    dropdownContent.style.display =
+      dropdownContent.style.display === "block" ? "none" : "block";
+  }
+
+  handleDocumentClick(event) {
+    if (!this.contains(event.target)) {
+      this.shadowRoot.querySelector(".dropdown-content").style.display = "none";
+    }
+  }
+
+  render() {
+    const container = this.shadowRoot.querySelector(".multi-select-container");
+    container.innerHTML = "";
+
+    const optgroups = this.querySelectorAll("optgroup");
+
+    optgroups.forEach((optgroup) => {
+      const groupDiv = document.createElement("div");
+      groupDiv.className = "optgroup";
+
+      const groupLabel = document.createElement("div");
+      groupLabel.textContent = optgroup.label;
+      groupDiv.appendChild(groupLabel);
+
+      const options = optgroup.querySelectorAll("option");
+      options.forEach((option) => {
+        const optionDiv = document.createElement("label");
+        optionDiv.className = "option";
+
+        const checkbox = document.createElement("input");
+        checkbox.type = "checkbox";
+        checkbox.value = option.value;
+        checkbox.name = this.getAttribute("name");
+        if (option.hasAttribute("selected")) {
+          checkbox.checked = true;
+          optionDiv.classList.add("selected");
+        }
+        checkbox.addEventListener("change", () => {
+          this.handleCheckboxChange(checkbox);
+        });
+
+        optionDiv.appendChild(checkbox);
+        optionDiv.appendChild(document.createTextNode(option.textContent));
+        groupDiv.appendChild(optionDiv);
+      });
+
+      container.appendChild(groupDiv);
+    });
+
+    this._updateSelect();
+  }
+
+  handleCheckboxChange(checkbox) {
+    const opt = this.querySelector(`option[value="${checkbox.value}"]`);
+    const isSingle = opt.hasAttribute("single");
+    if (!checkbox.checked) {
+      checkbox.parentElement.classList.remove("selected");
+    } else {
+      checkbox.parentElement.classList.add("selected");
+      // Gestion de l'option "single"
+      if (isSingle) {
+        // Uncheck all other checkboxes
+        const checkboxes = this.shadowRoot.querySelectorAll(
+          'input[type="checkbox"]'
+        );
+        checkboxes.forEach((cb) => {
+          if (cb !== checkbox) {
+            cb.checked = false;
+            cb.parentElement.classList.remove("selected");
+          }
+        });
+      } else {
+        // Uncheck the single checkbox if present
+        const singleCheckbox = Array.from(
+          this.shadowRoot.querySelectorAll('input[type="checkbox"]')
+        ).find((cb) =>
+          this.querySelector(`option[value="${cb.value}"]`).hasAttribute(
+            "single"
+          )
+        );
+        if (singleCheckbox) {
+          singleCheckbox.checked = false;
+          singleCheckbox.parentElement.classList.remove("selected");
+        }
+      }
+    }
+    this._updateSelect();
+  }
+
+  _updateSelect() {
+    const checkboxes = this.shadowRoot.querySelectorAll(
+      'input[type="checkbox"]'
+    );
+    const checkedBoxes = Array.from(checkboxes).filter(
+      (checkbox) => checkbox.checked
+    );
+
+    const values = checkedBoxes.map((checkbox) => checkbox.value);
+
+    const opts = checkedBoxes.map((checkbox) => {
+      return this.querySelector(`option[value="${checkbox.value}"]`);
+    });
+
+    const btn = this.shadowRoot.querySelector(".dropdown-button");
+
+    if (checkedBoxes.length === 0) {
+      btn.textContent = this.label || "Select options";
+    } else if (checkedBoxes.length < 4) {
+      btn.textContent = opts.map((opt) => opt.textContent).join(", ") + "";
+    } else {
+      btn.textContent = `${checkedBoxes.length} sélections`;
+    }
+
+    btn.textContent += " ⮛";
+
+    this._values(values);
+
+    this.dispatchEvent(new Event("change"));
+  }
+
+  _values(newValues = null) {
+    const checkboxes = this.shadowRoot.querySelectorAll(
+      'input[type="checkbox"]'
+    );
+    if (newValues === null) {
+      // Get selected values
+      const values = Array.from(checkboxes)
+        .filter((checkbox) => checkbox.checked)
+        .map((checkbox) => checkbox.value);
+      if (this.exportFormat) {
+        return this.exportFormat(values);
+      }
+      return values;
+    } else {
+      // Set selected values
+      checkboxes.forEach((checkbox) => {
+        checkbox.checked = newValues.includes(checkbox.value);
+      });
+
+      this._internals.setFormValue(this._values());
+    }
+  }
+
+  get value() {
+    return this._values();
+  }
+
+  set value(values) {
+    this._values(values);
+  }
+
+  on(callback) {
+    this.addEventListener("change", callback);
+  }
+
+  format(callback) {
+    this.exportFormat = callback;
+  }
+}
+
+customElements.define("multi-select", MultiSelect);
diff --git a/app/templates/assiduites/pages/signal_assiduites_group.j2 b/app/templates/assiduites/pages/signal_assiduites_group.j2
index fddae39c170aef54684d64a2ca0c4b1c09fe3bdd..b939944a7768dc1c7eb88a585982a4d10924c59a 100644
--- a/app/templates/assiduites/pages/signal_assiduites_group.j2
+++ b/app/templates/assiduites/pages/signal_assiduites_group.j2
@@ -7,7 +7,6 @@
 
 {% block scripts %}
 {{ super() }}
-<script src="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js"></script>
 <script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
 <script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
 <script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
@@ -73,9 +72,10 @@
         creerTousLesEtudiants(etuds);
     });
 
-    $("#group_ids_sel").on("change", ()=>{
+    const group_sel = document.querySelector("#group_ids_sel")
+    group_sel.on((values)=>{
         main();
-    })
+    });
 
     const moduleimpls = {};
     const inscriptionsModules = new Map();
@@ -83,7 +83,8 @@
 
     async function main(){
         dateCouranteEstTravaillee();
-        etuds = await recupEtuds($('#group_ids_sel').val());
+        let group_ids = group_sel.value;
+        etuds = await recupEtuds(group_ids);
         if (etuds.size != 0){
             await recupAssiduites(etuds, $("#date").datepicker("getDate"));
         }
@@ -101,8 +102,7 @@
 
 {% block styles %}
 {{ super() }}
-<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap-theme.min.css">
-<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
+{# <link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.min.css"> #}
 <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
 <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
 
@@ -153,12 +153,12 @@
         <div class="infos">
             <div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div>
             <div>
-            <button class="btn_date" onclick="jourSuivant(true)">
+            <button class="btn_date btn btn-secondary" onclick="jourSuivant(true)">
             &LeftArrowBar;
             </button>
             <input type="text" name="date" id="date" class="datepicker" value="{{date}}">
             </div>
-            <button class="btn_date" onclick="jourSuivant(false)">
+            <button class="btn_date btn btn-secondary" onclick="jourSuivant(false)">
             &RightArrowBar;
             </button>
         </div>
@@ -166,8 +166,8 @@
     <div style="display: {{'none' if readonly == 'true' else 'block'}};">
     {{timeline|safe}}
         <div>
-            <button onclick="setPeriodValues(t_start, t_mid)">Matin</button>
-            <button onclick="setPeriodValues(t_mid, t_end)">Après-Midi</button>
+            <button class=" btn btn-secondary" onclick="setPeriodValues(t_start, t_mid)">Matin</button>
+            <button class=" btn btn-secondary" onclick="setPeriodValues(t_mid, t_end)">Après-Midi</button>
         </div>
     </div>
 
diff --git a/app/templates/babase.j2 b/app/templates/babase.j2
index 2dc3aa89a0238ae15879374220d1b73fdfa39587..94baa48db66ad87df9ec16dec2094822ca11b0cf 100644
--- a/app/templates/babase.j2
+++ b/app/templates/babase.j2
@@ -33,6 +33,7 @@
     <script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
     <script src="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
     <script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
+    <script src="{{scu.STATIC_DIR}}/js/multi-select.js"></script>
     <script>
     const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
     </script>
diff --git a/app/views/assiduites.py b/app/views/assiduites.py
index 10e092cba1a02a0f72b5ab4481f4121fc4ffe7dd..c1d210ab655c20e555b13d2984b97f8afc5d6faf 100644
--- a/app/views/assiduites.py
+++ b/app/views/assiduites.py
@@ -94,7 +94,7 @@ from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
 from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
 
 
-CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
+CSSSTYLES = html_sco_header.BOOTSTRAP_CSS
 
 
 # --------------------------------------------------------------------
@@ -1164,7 +1164,7 @@ def signal_assiduites_group():
         formsemestre_date_fin=str(formsemestre.date_fin),
         formsemestre_id=formsemestre_id,
         gr_tit=gr_tit,
-        grp=sco_groups_view.menu_groups_choice(groups_infos),
+        grp=sco_groups_view.menu_groups_choice(groups_infos, html_export=False),
         minitimeline=_mini_timeline(),
         moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
         nonworkdays=_non_work_days(),
diff --git a/app/views/groups.py b/app/views/groups.py
index ae6bca0e7cc562ea246a032b6f5079c7cbae69dd..4f8e815dc0afb856476942dd5bed350219c5d9b9 100644
--- a/app/views/groups.py
+++ b/app/views/groups.py
@@ -45,7 +45,7 @@ def formulaire_feuille_appel():
 
     formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
 
-    group_ids: list[int] = request.args.get("group_ids", "").split(",")
+    group_ids: list[int] = request.args.getlist("group_ids")
 
     form: groups_form.FeuilleAppelPreForm = groups_form.FeuilleAppelPreForm(
         request.form
@@ -65,7 +65,7 @@ def formulaire_feuille_appel():
             "ens": form.ens.data or "",
         }
 
-        form_group_ids: list[str] = request.form.getlist("group_ids")
+        form_group_ids: list[str] = request.form.get("group_ids", "").split(",")
         if form_group_ids:
             groups_infos = DisplayedGroupsInfos(
                 form_group_ids,
@@ -89,5 +89,5 @@ def formulaire_feuille_appel():
         sco_data=ScoData(formsemestre=formsemestre),
         form=form,
         group_name=groups_infos.groups_titles,
-        grp=menu_groups_choice(groups_infos),
+        grp=menu_groups_choice(groups_infos, html_export=False),
     )