Skip to content

Dropping Bootstrap #1253

@jonfroehlich

Description

@jonfroehlich

I asked Claude about whether or not we should now completely drop our Bootstrap 3.3.6 dependency given our recent design-tokens.css refactor. Response: https://claude.ai/share/6ae8d3a1-461e-45ff-84e7-cbdd2efe2dd5

Bootstrap Migration Analysis: Makeability Lab Website

Date: January 2, 2026
Current Stack: Bootstrap 3.3.6 + jQuery 1.9.1
Recommendation: Drop Bootstrap, go fully custom with vanilla CSS/JS


Executive Summary

The Makeability Lab website has evolved to the point where Bootstrap is more burden than benefit. With design-tokens.css and modern CSS (flexbox/grid) already powering the newer components, you're essentially maintaining two styling systems. Removing Bootstrap eliminates ~200KB of overridden CSS/JS, removes jQuery security concerns, and simplifies the codebase.


Current Bootstrap Usage Analysis

Component Inventory

Component Usage Count Current State
Grid system ~242 instances Mixed - newer pages use CSS Grid
Navbar 1 (responsive collapse) Works but dated
Carousel 2 (hero + project pages) Functional
Popover Citations only Already has custom JS wrapper
ScrollSpy Publications TOC Single use
Responsive utilities ~30 (hidden-xs, etc.) Trivial to replace

Grid Class Distribution

col-md-*   : Most common (main layouts)
col-xs-*   : Mobile overrides
col-sm-*   : Tablet breakpoints  
col-lg-*   : Large screen tweaks
container  : Page wrappers
row        : Grid rows

JavaScript Dependencies

// Bootstrap JS components in use:
$('.carousel').carousel();           // Hero banners
$(trigger).popover('toggle');        // Citations
data-spy="scroll"                    // Publications TOC
data-toggle="collapse"               // Mobile navbar

Why NOT Upgrade to Bootstrap 5

Factor Bootstrap 3 → 5 Migration Drop Bootstrap Entirely
Class name changes Extensive (col-xs-*col-*) One-time replacement
JS rewrite Still needed (new API) Already have custom code
Bundle size ~150KB CSS + JS 0KB added
jQuery Can drop, but new learning curve Already removing
Long-term maintenance Still external dependency Full control

Verdict: Migration effort is similar, but dropping Bootstrap yields better long-term outcomes.


Migration Strategy

Phase 1: Remove jQuery (Week 1)

Risk: Low | Impact: High

jQuery 1.9.1 (2013) has known security vulnerabilities. Replace with vanilla JS.

// Before
$('.carousel').carousel();
$('.navbar-toggle').on('click', function() { ... });

// After
document.querySelector('.carousel'); // CSS handles transitions
document.querySelector('.navbar-toggle').addEventListener('click', ...);

Files to update:

  • website/templates/website/base.html (remove jQuery script tag)
  • website/static/website/js/top-navbar.js
  • website/static/website/js/citationPopoverSimple.js

Phase 2: Replace Bootstrap JS Components (Week 1-2)

Navbar Toggle (~20 lines)

// static/website/js/navbar.js
const NavbarToggle = (function() {
  'use strict';
  
  function init() {
    const toggle = document.querySelector('.navbar-toggle');
    const collapse = document.querySelector('.navbar-collapse');
    
    if (!toggle || !collapse) return;
    
    toggle.addEventListener('click', () => {
      const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
      toggle.setAttribute('aria-expanded', !isExpanded);
      collapse.classList.toggle('in');
    });
    
    // Close on outside click
    document.addEventListener('click', (e) => {
      if (!e.target.closest('.navbar') && collapse.classList.contains('in')) {
        collapse.classList.remove('in');
        toggle.setAttribute('aria-expanded', 'false');
      }
    });
  }
  
  return { init };
})();

document.addEventListener('DOMContentLoaded', NavbarToggle.init);

Carousel (~60 lines)

// static/website/js/carousel.js
const Carousel = (function() {
  'use strict';
  
  function init(selector, options = {}) {
    const carousel = document.querySelector(selector);
    if (!carousel) return;
    
    const slides = carousel.querySelectorAll('.carousel-item');
    const indicators = carousel.querySelectorAll('.carousel-indicator');
    const interval = options.interval || 10000;
    let currentIndex = 0;
    let timer = null;
    
    function showSlide(index) {
      slides.forEach((slide, i) => {
        slide.classList.toggle('active', i === index);
      });
      indicators.forEach((ind, i) => {
        ind.classList.toggle('active', i === index);
        ind.setAttribute('aria-selected', i === index);
      });
      currentIndex = index;
    }
    
    function next() {
      showSlide((currentIndex + 1) % slides.length);
    }
    
    function startAutoplay() {
      if (slides.length > 1) {
        timer = setInterval(next, interval);
      }
    }
    
    function stopAutoplay() {
      clearInterval(timer);
    }
    
    // Pause on hover/focus
    carousel.addEventListener('mouseenter', stopAutoplay);
    carousel.addEventListener('mouseleave', startAutoplay);
    carousel.addEventListener('focusin', stopAutoplay);
    carousel.addEventListener('focusout', startAutoplay);
    
    // Indicator clicks
    indicators.forEach((ind, i) => {
      ind.addEventListener('click', () => showSlide(i));
    });
    
    startAutoplay();
  }
  
  return { init };
})();

ScrollSpy with IntersectionObserver (~30 lines)

// static/website/js/scrollspy.js
const ScrollSpy = (function() {
  'use strict';
  
  function init(tocSelector, contentSelector) {
    const toc = document.querySelector(tocSelector);
    const headings = document.querySelectorAll(`${contentSelector} h2[id], ${contentSelector} h3[id]`);
    
    if (!toc || !headings.length) return;
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        const link = toc.querySelector(`a[href="#${entry.target.id}"]`);
        if (link) {
          link.classList.toggle('is-active-link', entry.isIntersecting);
        }
      });
    }, {
      rootMargin: '-80px 0px -80% 0px'
    });
    
    headings.forEach(heading => observer.observe(heading));
  }
  
  return { init };
})();

Phase 3: Replace Grid System (Week 2-3)

New Utility Classes

Add to design-tokens.css:

/* =============================================================================
   LAYOUT UTILITIES (Bootstrap Grid Replacement)
   ============================================================================= */

/* Container */
.container {
  width: 100%;
  max-width: var(--container-max-width, 1200px);
  margin-inline: auto;
  padding-inline: var(--space-4);
}

.container-fluid {
  width: 100%;
  padding-inline: var(--space-4);
}

/* Flexbox Row */
.row {
  display: flex;
  flex-wrap: wrap;
  margin-inline: calc(var(--space-4) * -0.5);
}

.row > * {
  padding-inline: calc(var(--space-4) * 0.5);
}

/* Grid-based columns (simpler than Bootstrap's 12-col) */
.grid {
  display: grid;
  gap: var(--space-4);
}

.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }

/* Common layout patterns */
.layout-sidebar {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
}

@media (min-width: 992px) {
  .layout-sidebar {
    grid-template-columns: 250px 1fr;
  }
  
  .layout-sidebar-right {
    grid-template-columns: 1fr 300px;
  }
}

.layout-8-4 {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}

@media (min-width: 992px) {
  .layout-8-4 {
    grid-template-columns: 2fr 1fr;
  }
}

/* Responsive visibility */
.hide-mobile { display: none; }
.hide-tablet { display: block; }
.hide-desktop { display: block; }

@media (min-width: 576px) {
  .hide-mobile { display: block; }
  .hide-tablet { display: none; }
}

@media (min-width: 992px) {
  .hide-tablet { display: block; }
  .hide-desktop { display: none; }
}

/* For cases where you need the 12-column grid */
.grid-12 {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-4);
}

.col-span-1 { grid-column: span 1; }
.col-span-2 { grid-column: span 2; }
.col-span-3 { grid-column: span 3; }
.col-span-4 { grid-column: span 4; }
.col-span-6 { grid-column: span 6; }
.col-span-8 { grid-column: span 8; }
.col-span-12 { grid-column: span 12; }

@media (min-width: 992px) {
  .md\:col-span-2 { grid-column: span 2; }
  .md\:col-span-4 { grid-column: span 4; }
  .md\:col-span-8 { grid-column: span 8; }
  .md\:col-span-10 { grid-column: span 10; }
}

Template Migration Pattern

<!-- Before (Bootstrap) -->
<div class="container">
  <div class="row">
    <div class="col-md-8">Main content</div>
    <div class="col-md-4 hidden-xs hidden-sm">Sidebar</div>
  </div>
</div>

<!-- After (Custom) -->
<div class="container">
  <div class="layout-8-4">
    <main>Main content</main>
    <aside class="hide-mobile hide-tablet">Sidebar</aside>
  </div>
</div>

Phase 4: Remove Bootstrap Files (Week 3)

  1. Delete static/website/css/bootstrap.css
  2. Delete static/website/css/bootstrap-theme.css
  3. Delete static/website/css/bootstrap-modifications.css
  4. Remove Bootstrap CDN script from base.html
  5. Remove jQuery CDN script from base.html
  6. Run full visual regression test

File-by-File Migration Checklist

Templates to Update

  • website/templates/website/base.html - Remove Bootstrap/jQuery, add new JS
  • website/templates/website/index.html - Carousel markup
  • website/templates/website/project.html - Carousel, grid
  • website/templates/website/member.html - Grid layout
  • website/templates/website/publications.html - Grid, ScrollSpy
  • website/templates/website/people.html - Grid (already mostly custom)
  • website/templates/website/project_listing.html - Grid (already mostly custom)
  • website/templates/website/news_item.html - Grid layout
  • website/templates/website/news_listing.html - Grid (already custom)
  • website/templates/snippets/display_pub_snippet.html - Column classes
  • website/templates/snippets/display_citation_link_snippet.html - Popover

JavaScript to Update/Create

  • Create static/website/js/navbar.js
  • Create static/website/js/carousel.js
  • Create static/website/js/scrollspy.js
  • Update static/website/js/citationPopoverSimple.js - Remove jQuery
  • Update static/website/js/top-navbar.js - Remove jQuery

CSS to Update

  • Extend design-tokens.css with layout utilities
  • Update top-navbar.css for vanilla collapse
  • Update publications.css for vanilla ScrollSpy states
  • Delete bootstrap.css, bootstrap-theme.css, bootstrap-modifications.css

Effort Estimate

Task Hours Priority
Vanilla navbar JS 2 High
Vanilla carousel JS 3 High
Vanilla popover (update existing) 2 Medium
Vanilla ScrollSpy 1 Medium
Layout utility CSS 2 High
Template grid migration (~15 files) 8-12 High
Testing & visual QA 4 High
Total 22-26 hours

Benefits Summary

Benefit Impact
Bundle size reduction -200KB (Bootstrap CSS + JS + jQuery)
Security Remove jQuery 1.9.1 vulnerabilities
Performance Faster page loads, no unused CSS
Maintainability Single styling system (design tokens)
Developer experience Modern CSS/JS, no framework quirks
Future-proofing No external dependency updates needed

Risks & Mitigations

Risk Likelihood Mitigation
Visual regressions Medium Screenshot comparison before/after each template
Edge cases in old browsers Low Your audience is academic/tech - modern browsers
Timeline slip Medium Migrate incrementally, keep Bootstrap until each page is done
Popover complexity Low Existing citationPopoverSimple.js handles most logic

Recommendation

Proceed with migration. The codebase is already 70% transitioned to custom CSS. The remaining work is straightforward, and the long-term benefits (security, performance, maintainability) justify the ~25 hour investment.

Suggested Timeline

  • Week 1: Remove jQuery, create vanilla JS components
  • Week 2: Migrate high-traffic pages (index, publications, people)
  • Week 3: Migrate remaining pages, remove Bootstrap, final QA

Appendix: Quick Reference

Bootstrap → Custom Class Mapping

Bootstrap Class Custom Replacement
container container (keep, redefine)
row row or grid or specific layout
col-md-8 md:col-span-8 or layout class
col-md-4 md:col-span-4 or layout class
hidden-xs hide-mobile
hidden-sm hide-tablet
hidden-md hidden-lg hide-desktop
visible-xs (use default + hide others)
pull-right margin-left: auto or flexbox
text-center text-center (keep)

New Layout Patterns

<!-- Two-column with sidebar -->
<div class="layout-sidebar">...</div>

<!-- 8-4 split -->
<div class="layout-8-4">...</div>

<!-- Equal columns -->
<div class="grid-3">...</div>

<!-- Custom 12-col when needed -->
<div class="grid-12">
  <div class="col-span-8 md:col-span-10">...</div>
  <div class="col-span-4 md:col-span-2">...</div>
</div>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions