Select2 on Bootstrap 4: Theme Migration Guide
Make a jQuery Select2 dropdown match Bootstrap 4 the elegant way: swap its theme for a maintained Bootstrap 4 theme in two lines instead of hand-rolling override CSS.
Published
Why the Default Theme Looks Wrong on Bootstrap 4
Select2 does not style your <select>. It hides the native
element (keeping it in the DOM for form submission and accessibility) and injects
its own markup right after it: a .select2-container holding a
.select2-selection, with the open dropdown appended to
<body>. Everything you see is that generated markup, styled by
whichever theme is active; out of the box that's --default,
which was drawn to sit next to Bootstrap 3 inputs. Beside a
Bootstrap 4 .form-control it is subtly but visibly off:
| Property | Select2 default | Bootstrap 4 .form-control |
|---|---|---|
| Height (single) | 28px | calc(1.5em + .75rem + 2px) = 38px |
| Border color | #aaa | #ced4da |
| Focus cue | blue glow (BS3-ish) | #80bdff border + 0 0 0 .2rem rgba(0,123,255,.25) |
| Text color | #444 | #495057 |
| Tag (multiple) | grey pill | looks nothing like a .badge |
Most teams fix that by hand: a scoping class and roughly 120 lines of
override CSS re-drawing heights, borders, focus rings, tags, sizes, and
validation states one by one. It works, and every value in it is
hard-won. It is also 120 lines you own forever, full of second-order details
(the caret height, the × on a blue tag, the line-height that
centers the label) that hand-rolled overrides routinely miss on the first pass.
The elegant fix uses the mechanism Select2 shipped for exactly this. Every class
Select2 generates embeds the theme name: .select2-container--default,
.select2-dropdown styled under --default rules. Pass
theme: 'bootstrap4' and the generated classes become
--bootstrap4 instead, so the Bootstrap 3-era rules never apply at
all. There is nothing to override, no specificity fight, no
!important: just a theme stylesheet that draws the widget as a
Bootstrap 4 control from the start.
The Whole Migration: One Stylesheet, Two Lines
The maintained Bootstrap 4 theme is the
select2-bootstrap4-theme
package (npm: @ttskch/select2-bootstrap4-theme). Add its stylesheet
after Select2's own:
<!-- Select2 core, then the Bootstrap 4 theme --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ttskch/[email protected]/dist/select2-bootstrap4.min.css">
Then tell Select2 to use it. Setting it as a global default converts every widget on the site in one line, which is exactly what you want mid-migration:
// Once, before any .select2() call: every widget on the site is themed. $.fn.select2.defaults.set('theme', 'bootstrap4'); $.fn.select2.defaults.set('width', '100%'); /* see Section 5: never leave width to chance */ $('select').select2();
// Or per widget, if you're converting incrementally: $('#country').select2({ theme: 'bootstrap4', width: '100%' });
That is the entire migration. The stylesheet is those ~120 hand-rolled lines,
written once by someone else, exercised by every project that uses the package,
and versioned so a Select2 point release doesn't silently break your form. If
your site bundles CSS instead of using a CDN, npm install
@ttskch/select2-bootstrap4-theme and import
dist/select2-bootstrap4.min.css; nothing else changes.
Live Comparison
theme: 'bootstrap4'.
These are static, non-interactive replicas of Select2's real DOM, styled by the
two real stylesheets linked from this page; the only difference between the
columns is the theme name in the generated classes, which is the only difference
on a real page too. The bottom row shows each theme's focus/open state.
What You Get for Free
Everything the hand-rolled approach had to reconstruct line by line, the theme ships as a matched set:
-
Control metrics. The single select lands on Bootstrap's exact
calc(1.5em + .75rem + 2px)height with the label line-height and caret aligned to it; the hardest part of a manual port is the part you never think about again. -
The focus ring.
#80bdffborder plus the softrgba(0,123,255,.25)shadow on both--focusand--open, single and multiple alike. -
Tags, dropdown, search box, results. The multiple select's
pills, the panel border and radius, the inline search field, and the
highlighted/selected option states all follow Bootstrap 4's palette,
including the dropdown, which lives on
<body>where page-level wrapper selectors can't reach it. -
Sizing. Small and large variants that track
.form-control-sm/.form-control-lgmetrics, so a themed Select2 can sit in a compact filter row or a large hero form. -
Validation states. Bootstrap's
.is-invalid/.is-validon the (hidden) native select are forwarded to the visible container: red or green border, matching focus shadow. Manually, this takes sibling-combinator gymnastics; here it's just on.
If your brand deviates from stock Bootstrap 4 (custom $primary,
different radii), don't fall back to hand-rolling: the theme is SCSS that imports
Bootstrap's variables. Build it with your own _variables.scss and it
inherits your brand the same way your buttons do. Custom CSS on top of the theme
should be reserved for genuine one-offs, and each one scoped to
.select2-container--bootstrap4.
#007bff, which is only about
4:1 contrast, below the WCAG AA threshold of 4.5:1 for normal
text; Bootstrap 4's own .badge-primary has the same problem. If your
accessibility audit flags it (this site's did, on this very page), one scoped rule
fixes it: darken .select2-results__option--highlighted to
#0062cc, the shade Bootstrap 4 uses for :active. It
reads as the same blue and clears AA at ~5.8:1.
The Three Traps the Theme Can't Fix
A theme is CSS; these are behavioral, so they stay on your side of the line no matter how the widget is skinned.
Select2 computes width from the original <select>. If that
element is inside a hidden container at init time (a collapsed
.tab-pane, a closed modal, an accordion), its width resolves to
0 and the widget renders as a sliver. Set it explicitly, ideally
in the same global defaults call as the theme:
$.fn.select2.defaults.set('width', '100%'); /* '100%' lets the container fill its parent, so Bootstrap's grid (.col-*, .form-group) controls width the way it does for a real .form-control. Avoid width:'resolve' inside hidden containers. */
The results panel is appended to <body>, so inside a Bootstrap
modal it renders behind the backdrop or clips. Point it at the modal's stacking
context instead:
$('#country').select2({ dropdownParent: $('#myModal') });
Inside a Bootstrap .input-group, the Select2 container is an
inline-block span, not a flex child, so it can break the group's
rounded-corner joins. If your layout hits this, it is the one place a few lines
of CSS are still warranted: make the container a flex item
(flex: 1 1 auto; width: 1%) and zero the border radius on the joined
side, scoped to .input-group > .select2-container--bootstrap4.
That's the entire custom-CSS budget of this approach.
Select2 Pitfalls Worth Knowing Before You Commit
The theme makes Select2 look right. These are the deeper trade-offs that no stylesheet fixes, most stemming from the same root cause: Select2 renders a parallel DOM and depends on jQuery.
-
Accessibility gaps in v4. Select2 4.x has long-standing
screen-reader issues (
aria-activedescendant/announcement quirks with the search box). If accessibility is a hard requirement, test with a real screen reader before shipping. - jQuery is mandatory, and Bootstrap moved on. Bootstrap 5 dropped jQuery entirely. Select2 has no official Bootstrap 5 build, so a Select2 + BS5 pairing means carrying jQuery solely for the dropdown and a community theme, indefinitely.
-
SPA framework friction. In React/Vue/Angular, Select2's
direct DOM manipulation fights the virtual DOM; you must manually
.select2()on mount and.select2('destroy')on unmount, and value binding is awkward. - The theme is third-party. The trade of the elegant approach: you depend on a community package tracking two upstreams. It is a stable, widely used one, and the version pin means it can only break when you choose to bump it, but if your policy forbids third-party CSS entirely, hand-rolling the ~120 lines yourself is the remaining option, and the values in Section 1's table are where to start.
None of these are dealbreakers on an existing jQuery + Bootstrap 4 app. They are reasons not to reach for Select2 on something new.
Select2 vs. the Alternatives
If you are choosing today rather than maintaining an existing widget, weigh Select2 against the vanilla-JS options that have largely superseded it.
| Library | Dependency | Size (min+gzip, approx) | Bootstrap 4 / 5 | Best for |
|---|---|---|---|---|
| Select2 4.x | jQuery (required) | ~24 KB + jQuery ~30 KB | BS4 via the theme in this guide; no official BS5 | Existing jQuery/BS4 apps; AJAX remote data, custom option templates, tag creation |
| Tom Select | None (vanilla) | ~16 KB | Ships BS4 and BS5 stylesheets | The modern default; a near drop-in successor to Selectize with remote loading and plugins |
| Choices.js | None (vanilla) | ~19 KB | Style manually; no bundled BS theme | Lightweight multi/tags without jQuery when you're happy to theme it yourself |
| Selectize.js | Standalone (jQuery optional) | ~24 KB | Community themes; dated | Legacy apps already on it; new work should prefer Tom Select |
| Native <select> + <datalist> | None | 0 KB | Fully native; option list not stylable cross-browser | Simple single-select; best performance and accessibility, no rich tag UI or searchable multi-select |
A quick decision rule:
- Already on jQuery + Bootstrap 4 and it works? Keep Select2, add the theme from Section 2, and move on. A rewrite rarely pays for itself.
- New build, or heading toward Bootstrap 5? Choose Tom Select. No jQuery, ships BS4/BS5 CSS, actively maintained.
- You only need a searchable single value and can live without a styled option list? A native
<select>(optionally<datalist>) beats every library on size and accessibility. - Need remote/AJAX search or per-option templates and prefer no jQuery? Tom Select or Choices.js both cover it.