Fast-forward Bootstrap 3 to Bootstrap 5

bootstrap logo

Part 4 of a series. Start with Part 1 if you’re just joining.

If the previous post was about deciding to migrate from Bootstrap 3 to Bootstrap 5, this one is about actually doing it.

The work broke down into four distinct areas: the .info.yml updates, the template rewrites, the CSS changes, and the block region remapping. These are mostly independent of each other, which means you can tackle them in whatever order makes sense — though I’d recommend doing the .info.yml and region remapping together, since switching the base theme immediately breaks block placements and you’ll want to fix them in the same pass.

The .info.yml Changes

Every custom subtheme that extends drupal/bootstrap has a .info.yml file that declares the base theme and any library extensions. For Bootstrap 5, those declarations change.

The key differences: - base theme: bootstrap becomes base theme: bootstrap5 - The library extension changes from bootstrap/framework to bootstrap5/global-styling - The core_version_requirement should be updated to ^10 || ^11

For my thin subthemes (aircooled, svloba, el_hincho), this was the entire theme-level change other than the CSS work. No templates to update, just the declaration. The framework does the rest.

The Region Remapping Problem

Bootstrap 3’s theme and Bootstrap 5’s theme define different regions, and the region names for the navigation area are completely different. This is the part that trips people up, because block placements are stored in configuration tied to the region name. Switching the base theme without handling the region change means your navigation blocks disappear.

The mapping between old and new regions:

Old region (Bootstrap 3)

New region (Bootstrap 5)

navigation (site branding, logo)

nav_branding

navigation_collapsible (main menu)

nav_main

(no equivalent)

nav_additional (account menu, search, cart)

(no equivalent)

breadcrumb

The content, sidebar_first, sidebar_second, highlighted, help, footer, and other regions tend to be consistent between Bootstrap themes and don’t require remapping.

The critical blocks to move: - Site branding block (logo/site name): navigationnav_branding - Main navigation (main menu block): navigation_collapsiblenav_main - Account menu, search, cart (if you have e-commerce): these may have been jammed into navigation_collapsible in Bootstrap 3 but now belong in nav_additional

The Template Rewrite

The thin subthemes needed no template work. bootstrap_cis (careersinspanish) was a different story.

Its page.html.twig implemented a full custom Bootstrap 3 navbar. The markup was full of BS3-specific patterns:

<!-- Bootstrap 3 navbar pattern — does not work in BS5 --> <nav class="navbar navbar-default" role="navigation">  <div class="navbar-header">    <button type="button" class="navbar-toggle"            data-toggle="collapse"            data-target=".navbar-ex1-collapse">      <span class="sr-only">Toggle navigation</span>    </button>    {{ page.navigation }}  </div>  <div class="collapse navbar-collapse navbar-ex1-collapse">    {{ page.navigation_collapsible }}  </div> </nav>

Bootstrap 5 changes several things here: - data-toggledata-bs-toggle (all Bootstrap data attributes are now namespaced with bs-) - data-targetdata-bs-target - navbar-default → removed (Bootstrap 5 has navbar-light/navbar-dark plus bg-* utilities) - sr-onlyvisually-hidden (Bootstrap 5 renamed this class) - navbar-togglenavbar-toggler with navbar-toggler-icon inside - The collapse target syntax changes slightly - Region names change: navigationnav_branding, navigation_collapsiblenav_main

The rewrite for Bootstrap 5:

<!-- Bootstrap 5 navbar pattern --> <nav class="navbar navbar-expand-md navbar-light bg-light" role="navigation">  <div class="{{ b5_top_container }}">    {{ page.nav_branding }}    <button class="navbar-toggler" type="button"            data-bs-toggle="collapse"            data-bs-target="#navbarCollapse"            aria-controls="navbarCollapse"            aria-expanded="false"            aria-label="Toggle navigation">      <span class="navbar-toggler-icon"></span>    </button>    <div class="collapse navbar-collapse" id="navbarCollapse">      {{ page.nav_main }}      {{ page.nav_additional }}    </div>  </div> </nav>

Note the b5_top_container variable — Bootstrap 5’s Drupal theme provides this variable to control whether the navbar uses a full-width container or a fixed-width one. You configure it in the theme settings.

The system branding block template also needed attention. It had been extending block--bare.html.twig, a template that existed in the Bootstrap 3 theme but doesn’t exist in Bootstrap 5. Changing extends "block--bare.html.twig" to extends "block.html.twig" and updating the CSS classes fixed it.

The CSS Changes

This is the most open-ended part of the migration, because it depends entirely on what Bootstrap 3 classes your custom CSS was using or overriding.

The high-frequency changes:

Bootstrap 3

Bootstrap 5

col-xs-*

col-* (xs prefix dropped)

col-sm-*, col-md-*, etc.

Same names — unchanged

navbar-default

Removed — use navbar-light + bg-light

panel

card

panel-body

card-body

panel-heading

card-header

well

Removed — use bg-light p-3 rounded or similar

glyphicon-*

Removed entirely — Bootstrap 5 ships no icon font

sr-only

visually-hidden

pull-left / pull-right

float-start / float-end

hidden-xs, visible-sm, etc.

d-none d-sm-block etc. (display utilities)

Beyond class renames, Bootstrap 5 also changed the default flex direction for .navbar-nav outside of a .navbar context — it now defaults to column layout. If you have a footer navigation using .nav or .navbar-nav, you’ll likely need to add display:flex; flex-direction:row explicitly to keep it horizontal.

The approach I took was to read through each theme’s css/style.css looking for any class names on the BS3 list, then test visually. The visual test usually revealed a few things the text search missed — elements that relied on Bootstrap 3’s default styles rather than explicitly setting them.

Deploying the Theme Change

Switching a Drupal site from one theme to another in production requires more than just pushing code. The theme configuration and block placements are stored in the database, not in files. If you’re managing a multi-site deployment, you need a reliable way to apply these database changes to every subsite.

For this project, I wrote an idempotent deploy script that handles enabling the Bootstrap 5 theme and remapping all blocks on all subsites. Idempotent means it’s safe to run multiple times — if the blocks are already in the right regions, running the script again doesn’t break anything. That property is important for a deploy hook, where the script runs on every deployment.

The script handles: 1. Enabling the bootstrap5 theme on each subsite 2. Moving each named block to its correct Bootstrap 5 region with the correct weight 3. Setting the b5_top_container theme setting to container 4. Clearing caches on all subsites

Once this script was in place and tested locally, the production rollout was a single command.

Expert Mode: Bootstrap 3 to 5 Migration

Install Bootstrap 5:

ddev composer require drupal/bootstrap5

Update each subtheme’s .info.yml:

# Before base theme: bootstrap libraries-extend:  bootstrap/framework:    - mytheme/global-styling core_version_requirement: ^8 || ^9 || ^10 # After base theme: bootstrap5 libraries-extend:  bootstrap5/global-styling:    - mytheme/global-styling core_version_requirement: ^10 || ^11

Find all CSS files with Bootstrap 3 class names to review:

# Check for BS3-specific class names in theme CSS grep -r "col-xs\|navbar-default\|panel-body\|panel-heading\|\bwell\b\|glyphicon\|sr-only\|pull-left\|pull-right" \  web/themes/custom/*/css/

Find all Twig templates with BS3 data attributes:

grep -r "data-toggle\|data-target\|data-dismiss\|navbar-toggle\|navbar-collapse" \  web/themes/custom/*/templates/

Enable the Bootstrap 5 theme on a subsite and remap blocks via Drush:

# Enable the theme ddev drush --uri=aircooled theme:enable bootstrap5 # Set as default theme ddev drush --uri=aircooled config:set system.theme default bootstrap5 -y # Move the branding block to the correct region ddev drush --uri=aircooled config:set block.block.aircooled_branding region nav_branding -y # Move the main menu block ddev drush --uri=aircooled config:set block.block.aircooled_main_menu region nav_main -y # Move the account menu (if applicable) ddev drush --uri=aircooled config:set block.block.aircooled_account_menu region nav_additional -y # Clear cache ddev drush --uri=aircooled cr

List all blocks and their current regions for a subsite:

ddev drush --uri=aircooled config:get --format=table block.block # Or more targeted: ddev drush --uri=aircooled php:eval "  \$blocks = \Drupal::entityTypeManager()->getStorage('block')->loadMultiple();  foreach (\$blocks as \$id => \$block) {    echo \$id . ' => ' . \$block->getRegion() . PHP_EOL;  } "

Run the deploy script locally across all subsites:

DRUSH="ddev drush" bash scripts/deploy-bootstrap5.sh

Verify the theme is active and blocks are in the right regions:

for site in aircooled careersinspanish figueroa svloba; do  echo "=== $site ==="  ddev drush --uri=$site config:get system.theme  ddev drush --uri=$site cr  echo "Cache cleared" done

With the theme migration complete and all module version constraints updated, Phase 3 is done. Every known Drupal 11 blocker has been resolved. In the next post, we’ll do the actual upgrade.

 

Read the next post - And Now We’re Ready: Final Tests and Flipping the Switch