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

1

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:

PropertySelect2 defaultBootstrap 4 .form-control
Height (single)28pxcalc(1.5em + .75rem + 2px) = 38px
Border color#aaa#ced4da
Focus cueblue glow (BS3-ish)#80bdff border + 0 0 0 .2rem rgba(0,123,255,.25)
Text color#444#495057
Tag (multiple)grey pilllooks 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.

2

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.

3

Live Comparison

Left = Select2's default theme. Right = the same widget with 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.
Single select
Default
theme: 'bootstrap4'
Multiple select (tags)
Default
theme: 'bootstrap4'
Focus / open state
Default
theme: 'bootstrap4'
4

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. #80bdff border plus the soft rgba(0,123,255,.25) shadow on both --focus and --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-lg metrics, so a themed Select2 can sit in a compact filter row or a large hero form.
  • Validation states. Bootstrap's .is-invalid / .is-valid on 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.

One wart to audit: the theme paints the keyboard-highlighted result white on Bootstrap's raw primary, #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.
5

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.

1. Width: never leave it to Select2

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. */
2. Dropdowns inside modals

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')
});
3. Input group corner joins

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.

6

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.

7

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.