Remove jQuery from GitLabs Frontend code
# Summary
jQuery is a large legacy dependency that is no longer necessary in GitLab's frontend codebase. With our adoption of Vue.js, native browser APIs, and `@gitlab/ui` components, we have the tools to replace every jQuery usage with modern, maintainable alternatives. Removing jQuery will:
- Reduce the main JavaScript bundle size
- Improve code readability and long-term maintainability
- Eliminate a transitive dependency that blocks further modernization efforts
- Bring us closer to a fully Vue/native-DOM frontend architecture
## Scope
This epic tracks the removal of jQuery from **93 files** across `app/assets/javascripts/` and `ee/app/assets/javascripts/`, organized into **48 tracking issues** grouped into 5 execution phases.
The following are explicitly **out of scope** here and tracked separately:
| Related Epic | Scope |
|---|---|
| [#3419 - Remove Bootstrap JavaScript code](https://gitlab.com/groups/gitlab-org/-/epics/3419) | `commons/bootstrap.js`, `gl_crop.js` and coupled profile files -- jQuery removal guidance has been appended to that epic's description |
| [#4002 - Replace jquery.caret / jquery.atwho / At.js](https://gitlab.com/groups/gitlab-org/-/epics/4002) | GFM autocomplete system; `related_issuable_input.vue` and `set_status_form.vue` are blocked until `at.js` is replaced |
| `app/assets/javascripts/deprecated_notes.js` | Requires special handling, excluded for now |
## Common replacement patterns
A quick reference for developers working on these issues:
| jQuery | Vanilla JS |
|---|---|
| `$('.sel')` | `document.querySelector('.sel')` / `document.querySelectorAll('.sel')` |
| `$el.find('.child')` | `el.querySelector('.child')` |
| `$el.closest('.parent')` | `el.closest('.parent')` |
| `$el.addClass('foo')` / `.removeClass()` / `.toggleClass()` | `el.classList.add()` / `.remove()` / `.toggle()` |
| `$el.attr('href')` / `.attr('href', val)` | `el.getAttribute('href')` / `.setAttribute('href', val)` |
| `$el.data('key')` | `el.dataset.key` |
| `$el.val()` / `.val(v)` | `el.value` / `el.value = v` |
| `$el.on('click', handler)` | `el.addEventListener('click', handler)` |
| `$el.off('click', handler)` | `el.removeEventListener('click', handler)` |
| `$(document).on('click', '.sel', handler)` | `document.addEventListener('click', (e) => { if (e.target.closest('.sel')) handler(e); })` |
| `$el.trigger('custom')` | `el.dispatchEvent(new CustomEvent('custom'))` |
| `$(form).serialize()` | `new URLSearchParams(new FormData(form)).toString()` |
| `$(el).data('instance', obj)` | Use `WeakMap`: `instances.set(el, obj)` |
| `$el.show()` / `.hide()` | `toggleDisplay(el, true/false)` from `~/lib/utils/dom_utils` |
| `$(window).on('resize', fn)` | `new ResizeObserver(fn).observe(el)` |
| `.tooltip()` / `.modal()` / `.dropdown()` | `GlTooltip` / `GlModal` / `GlDisclosureDropdown` from `@gitlab/ui` |
## Reference MRs
The following merged MRs serve as reference implementations showing how jQuery removal has been approached in this codebase:
| MR | File(s) | Key patterns used |
|---|---|---|
| [!224776](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/224776) | `filterable_list.js` | `new URLSearchParams(new FormData(form)).toString()` replacing `$(form).serialize()` |
| [!224779](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/224779) | `deprecated_notes.js` | Partial/incremental reduction -- useful reference for files where full removal isn't yet possible |
| [!224961](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/224961) | `ee/groups/ldap_group_links.js` | `classList.toggle('!gl-hidden', condition)` |
| [!224971](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/224971) | `layout_nav.js` | `ResizeObserver` replacing `$(window).on('resize')`, `getBoundingClientRect()` |
| [!225189](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/225189) | `projects/project_find_file.js` | `document.createElement`, `dataset`, `replaceChildren()` |
| [!225357](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/225357) | `gl_form.js` | `WeakMap` for instance storage, `classList`, `querySelector`, `addEventListener` |
| [!225780](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/225780) | `behaviors/preview_markdown.js` | `CustomEvent` + `dispatchEvent`, `toggleDisplay()`, `.jquery` check for backward compat |
## Execution phases
Issues are organized into 5 phases based on complexity and inter-dependencies. Later phases have dependencies on earlier ones, so the recommended approach is to work through phases roughly in order.
| Phase | Epic | Description | Issues | Total weight |
|---|---|---|---|---|
| 1 | [Phase 1: Remove jQuery from independent, low-risk components](https://gitlab.com/groups/gitlab-org/-/epics/21547) | Independent files with straightforward jQuery usage. Can be worked on in parallel with no coordination needed. | 12 | 16 |
| 2 | [Phase 2: Remove jQuery from moderately coupled modules](https://gitlab.com/groups/gitlab-org/-/epics/21548) | Moderately complex files with some coupling. Careful migration required but no blocking dependencies. | 16 | 41 |
| 3 | [Phase 3: Remove jQuery from tightly coupled component clusters](https://gitlab.com/groups/gitlab-org/-/epics/21549) | Files tightly coupled via shared custom events, inherited classes, or cross-file dependencies. Requires coordinated migration across related files. | 9 | 21 |
| 4 | [Phase 4: Remove jQuery from heavy-dependency legacy modules](https://gitlab.com/groups/gitlab-org/-/epics/21550) | Heavily jQuery-dependent modules with deep coupling to legacy dropdown systems and complex DOM operations. Some may be better replaced wholesale with Vue components. | 8 | 30 |
| 5 | [Phase 5: Remove jQuery from foundational infrastructure and main entry point](https://gitlab.com/groups/gitlab-org/-/epics/21551) | Foundational files that must be tackled last: the GFM autocomplete system (blocked on `at.js` replacement), the deprecated jQuery dropdown (can be deleted once all consumers migrate), and `main.js` (removes the global `window.jQuery` assignment as the final step). | 3 | 14 |
**Total: 48 issues, 122 story points**
## How to approach an issue
1. Read the issue description for the specific files and implementation plan
2. Study the [reference MRs](#reference-mrs) above for patterns already used in this codebase
3. Use the [common replacement patterns](#common-replacement-patterns) table as a quick reference
4. Run the full frontend test suite after changes: `yarn jest` (including setting up fixtures) where applicable
5. Test the feature areas listed in each issue's **Test areas** section manually
> **Note on partial migrations:** For some files, complete jQuery removal may not be immediately possible due to deep coupling with jQuery plugins (e.g. `at.js`, Bootstrap jQuery plugins, `initDeprecatedJQueryDropdown`). In these cases:
> - Do not force a full removal if it risks introducing regressions
> - Leave a comment in the issue flagging the blocker and what needs to happen first (e.g. `at.js` replacement, Bootstrap plugin migration)
> - Partial reductions (removing jQuery where possible, leaving plugin-coupled code) are still valuable and should be submitted as separate MRs
The following issues are known candidates for partial-only migration -- please open a discussion in the issue before starting work to determine next steps:
| Issue | Reason full removal may not be immediately possible |
|---|---|
| [gitlab-org/gitlab#595397](https://gitlab.com/gitlab-org/gitlab/-/issues/595397) | GFM autocomplete -- blocked on `at.js` replacement ([Epic #4002](https://gitlab.com/groups/gitlab-org/-/epics/4002)) |
| [gitlab-org/gitlab#595385](https://gitlab.com/gitlab-org/gitlab/-/issues/595385) | Vue shared components -- `related_issuable_input.vue` and `set_status_form.vue` blocked on `at.js` |
| [gitlab-org/gitlab#595368](https://gitlab.com/gitlab-org/gitlab/-/issues/595368) | Notes store actions -- `clear-commands-cache.atwho` event consumed by `at.js`, partial only until `at.js` replaced |
| [gitlab-org/gitlab#595392](https://gitlab.com/gitlab-org/gitlab/-/issues/595392) | `labels_select.js` -- deeply coupled to deprecated jQuery dropdown, may require wholesale Vue replacement |
| [gitlab-org/gitlab#595394](https://gitlab.com/gitlab-org/gitlab/-/issues/595394) | `users_select/index.js` -- same as above, best replaced with a Vue-based component |
| [gitlab-org/gitlab#595398](https://gitlab.com/gitlab-org/gitlab/-/issues/595398) | Deprecated jQuery dropdown -- can only be deleted once all consumers are migrated away |
---
<details>
<summary>Old Description</summary>
jQuery is a rather large dependency which we don't utilize anymore due to our usage of Vue and other libraries.
On one hand we are using dependencies, which need jQuery. We should look into removing/replacing those in order to get rid of jQuery. These efforts are tracked here: https://gitlab.com/groups/gitlab-org/-/epics/3418
On the other hand we still have own Frontend code that utilizes jQuery. We should replace it with native DOM methods or Vue components. In order to go come up with a good migration plan we should consider the following action plans which can be driven in parallel:
Plan 1:
1. Removing usage of jQuery Ajax calls: https://gitlab.com/groups/gitlab-org/-/epics/1923
2. Removing usage of jQuery animations: TBA
3. Replace jQuery with jQuery slim (no animations / no ajax) => Smaller build
Plan 2:
1. Move jQuery out of the main bundle and add it directly to the pages that use it.
2. Remove jQuery from any page that uses it.
</details>
epic