Bootstrap 5 Responsive Bar Chart with Trend Line
A responsive bar-and-line chart drawn in plain SVG, comparing two revenue streams by month against a trend line. Legend buttons toggle each series and hovering or focusing a month shows its values.
<section class="wdw-stage">
<div class="container" style="max-width:960px">
<div class="wdw-card p-4 p-md-5">
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-4">
<div>
<span class="wdw-eyebrow">Revenue overview</span>
<h1 class="wdw-heading h3 mt-1 mb-1">Monthly performance</h1>
<p class="wdw-muted small mb-0">Subscriptions and one-off sales against the 3-month trend.</p>
</div>
<div class="d-flex flex-wrap gap-2" role="group" aria-label="Toggle datasets" id="chartLegend">
<button type="button" class="btn rounded-pill legend-btn active" data-series="0" aria-pressed="true"><span class="dot dot-a"></span>Subscriptions</button>
<button type="button" class="btn rounded-pill legend-btn active" data-series="1" aria-pressed="true"><span class="dot dot-b"></span>One-off</button>
<button type="button" class="btn rounded-pill legend-btn active" data-series="2" aria-pressed="true"><span class="dot dot-c"></span>Trend</button>
</div>
</div>
<div class="chart-wrap position-relative">
<svg id="barChart" class="w-100" role="group" aria-label="Bar chart of subscriptions and one-off revenue by month, with a trend line"></svg>
<div class="chart-tip" id="chartTip" role="status" aria-live="polite"></div>
</div>
</div>
</div>
</section>
/* Web Designer Wall library style — shared base for every Bootstrap 5 snippet.
Indigo accent, Plus Jakarta Sans, soft cards, pill buttons. Works in light and dark. */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap");
:root,
[data-bs-theme="light"] {
--wdw-font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--bs-body-font-family: var(--wdw-font);
--bs-primary: #4f46e5;
--bs-primary-rgb: 79, 70, 229;
--bs-primary-text-emphasis: #3730a3;
--bs-primary-bg-subtle: #eef2ff;
--bs-primary-border-subtle: #c7d2fe;
--bs-link-color: #4f46e5;
--bs-link-color-rgb: 79, 70, 229;
--bs-link-hover-color: #4338ca;
--bs-link-hover-color-rgb: 67, 56, 202;
--bs-focus-ring-color: rgba(79, 70, 229, 0.25);
--bs-border-radius: 0.75rem;
--bs-border-radius-lg: 1rem;
--bs-border-radius-xl: 1.25rem;
--wdw-radius-card: 1.25rem;
--wdw-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 16px 40px -16px rgba(16, 24, 40, 0.22);
--wdw-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px -4px rgba(16, 24, 40, 0.12);
--wdw-stage: #f5f6fb;
}
[data-bs-theme="dark"] {
--bs-primary: #818cf8;
--bs-primary-rgb: 129, 140, 248;
--bs-primary-text-emphasis: #c7d2fe;
--bs-primary-bg-subtle: rgba(129, 140, 248, 0.14);
--bs-primary-border-subtle: rgba(129, 140, 248, 0.35);
--bs-link-color: #a5b4fc;
--bs-link-color-rgb: 165, 180, 252;
--bs-link-hover-color: #c7d2fe;
--bs-link-hover-color-rgb: 199, 210, 254;
--bs-focus-ring-color: rgba(129, 140, 248, 0.35);
--wdw-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 40px -16px rgba(0, 0, 0, 0.7);
--wdw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
--wdw-stage: #0f1117;
}
body { font-family: var(--wdw-font); -webkit-font-smoothing: antialiased; }
/* Bootstrap compiles its blue into these components; point them at the accent. */
.btn-primary {
--bs-btn-bg: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: #4338ca; --bs-btn-hover-border-color: #4338ca;
--bs-btn-active-bg: #3730a3; --bs-btn-active-border-color: #3730a3;
--bs-btn-disabled-bg: var(--bs-primary); --bs-btn-disabled-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
[data-bs-theme="dark"] .btn-primary { --bs-btn-color: #0f1117; --bs-btn-hover-bg: #a5b4fc; --bs-btn-hover-border-color: #a5b4fc; --bs-btn-hover-color: #0f1117; --bs-btn-active-color: #0f1117; }
.btn-outline-primary {
--bs-btn-color: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-primary); --bs-btn-hover-border-color: var(--bs-primary);
--bs-btn-active-bg: var(--bs-primary); --bs-btn-active-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
.btn { --bs-btn-font-weight: 600; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .5rem; }
.btn-sm { min-height: 36px; }
.form-control, .form-select { min-height: 44px; border-radius: .75rem; }
.form-control:focus, .form-select:focus, .form-check-input:focus { border-color: rgba(var(--bs-primary-rgb), .6); box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.form-check-input:checked { background-color: var(--bs-primary); border-color: var(--bs-primary); }
.nav-pills { --bs-nav-pills-link-active-bg: var(--bs-primary); }
.nav { --bs-nav-link-color: var(--bs-secondary-color); --bs-nav-link-hover-color: var(--bs-emphasis-color); }
.pagination { --bs-pagination-active-bg: var(--bs-primary); --bs-pagination-active-border-color: var(--bs-primary); --bs-pagination-color: var(--bs-body-color); --bs-pagination-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.progress, .progress-stacked { --bs-progress-bar-bg: var(--bs-primary); }
.list-group { --bs-list-group-active-bg: var(--bs-primary); --bs-list-group-active-border-color: var(--bs-primary); }
.dropdown-menu { --bs-dropdown-link-active-bg: var(--bs-primary); border-radius: .875rem; box-shadow: var(--wdw-shadow-sm); }
.accordion { --bs-accordion-active-bg: var(--bs-primary-bg-subtle); --bs-accordion-active-color: var(--bs-primary-text-emphasis); --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.badge { font-weight: 600; }
/* Library building blocks — use these instead of re-inventing them per snippet. */
.wdw-stage { min-height: 100vh; display: flex; align-items: center; background: var(--wdw-stage); padding-block: 3rem; }
.wdw-card { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: var(--wdw-radius-card); box-shadow: var(--wdw-shadow); }
.wdw-card-sm { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: 1rem; box-shadow: var(--wdw-shadow-sm); }
.wdw-aurora { position: relative; color: #fff; background: radial-gradient(60% 55% at 100% 0, #818cf8 0, transparent 70%), radial-gradient(65% 60% at 0 100%, #ec4899 0, transparent 65%), #1e1b4b; }
.wdw-eyebrow { display: inline-block; font-size: .75rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--bs-primary); }
.wdw-heading { font-weight: 800; letter-spacing: -.02em; color: var(--bs-emphasis-color); }
.wdw-muted { color: var(--bs-secondary-color); }
.wdw-icon { width: 2.75rem; height: 2.75rem; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: .875rem; background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
/* Component */
.legend-btn { --bs-btn-border-color: var(--bs-border-color); --bs-btn-hover-border-color: var(--bs-primary); --bs-btn-hover-bg: var(--bs-tertiary-bg); --bs-btn-active-border-color: var(--bs-border-color); --bs-btn-color: var(--bs-body-color); border: 1px solid var(--bs-border-color); background: var(--bs-body-bg); min-height: 44px; padding-inline: .9rem; }
.legend-btn:not(.active) { opacity: .5; text-decoration: line-through; }
.legend-btn:hover { border-color: var(--bs-primary); }
.dot { width: .65rem; height: .65rem; border-radius: 50%; display: inline-block; }
.dot-a { background: var(--bs-primary); }
.dot-b { background: rgba(var(--bs-primary-rgb), .45); }
[data-bs-theme="dark"] .dot-b { background: rgba(var(--bs-primary-rgb), .65); }
.dot-c { background: var(--bs-secondary-color); }
#barChart { display: block; height: auto; overflow: visible; }
#barChart text { font-family: var(--wdw-font); font-size: 12px; fill: var(--bs-secondary-color); }
#barChart .grid { stroke: var(--bs-border-color); stroke-dasharray: 3 4; }
#barChart .bar-a { fill: var(--bs-primary); }
#barChart .bar-b { fill: rgba(var(--bs-primary-rgb), .45); }
[data-bs-theme="dark"] #barChart .bar-b { fill: rgba(var(--bs-primary-rgb), .65); }
#barChart .trend { fill: none; stroke: var(--bs-secondary-color); stroke-width: 2.5; stroke-linecap: round; }
#barChart .trend-dot { fill: var(--bs-body-bg); stroke: var(--bs-secondary-color); stroke-width: 2; }
#barChart rect.bar-a, #barChart rect.bar-b { transition: opacity .2s, height .6s cubic-bezier(.2,.8,.2,1), y .6s cubic-bezier(.2,.8,.2,1); }
#barChart g.dim rect, #barChart g.dim .trend-dot { opacity: .35; }
#barChart .hit { fill: transparent; cursor: pointer; }
.chart-wrap { min-width: 0; }
.chart-tip { opacity: 0; visibility: hidden; transition: opacity .15s; }
.chart-tip.show { opacity: 1; visibility: visible; }
.chart-tip { position: absolute; pointer-events: none; z-index: 2; background: var(--bs-emphasis-color); color: var(--bs-body-bg); border-radius: .75rem; padding: .6rem .8rem; font-size: .8rem; line-height: 1.5; box-shadow: var(--wdw-shadow-sm); transform: translate(-50%, -100%); white-space: nowrap; }
.chart-tip b { font-weight: 700; display: block; margin-bottom: 2px; }
@media (prefers-reduced-motion: reduce) { #barChart rect { transition: none !important; } }
(function () {
var svg = document.getElementById('barChart');
if (!svg) return;
var NS = 'http://www.w3.org/2000/svg';
var labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
var names = ['Subscriptions', 'One-off', 'Trend'];
var data = [
[42, 18, 48, 38, 46, 62, 35],
[24, 12, 22, 28, 13, 39, 42],
[30, 15, 34, 35, 40, 52, 48]
];
var visible = [true, true, true];
var tip = document.getElementById('chartTip');
var wrap = svg.parentNode;
var W, H, m = { t: 12, r: 8, b: 32, l: 40 }, ih, iw, max = 80;
function size() {
var narrow = wrap.clientWidth < 520;
W = Math.max(280, Math.round(wrap.clientWidth)); H = narrow ? 300 : 340;
ih = H - m.t - m.b; iw = W - m.l - m.r;
}
function el(name, attrs, parent) {
var n = document.createElementNS(NS, name);
for (var k in attrs) n.setAttribute(k, attrs[k]);
(parent || svg).appendChild(n);
return n;
}
function y(v) { return m.t + ih - (v / max) * ih; }
function draw() {
size();
svg.innerHTML = '';
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
for (var g = 0; g <= 4; g++) {
var v = g * 20;
el('line', { x1: m.l, x2: W - m.r, y1: y(v), y2: y(v), 'class': 'grid' });
var t = el('text', { x: m.l - 10, y: y(v) + 4, 'text-anchor': 'end' });
t.textContent = v;
}
var step = iw / labels.length;
var points = [];
labels.forEach(function (lab, i) {
var cx = m.l + step * i + step / 2;
var grp = el('g', { 'data-i': i });
var bw = Math.min(26, step * 0.3);
if (visible[0]) el('rect', { 'class': 'bar-a', x: cx - bw - 2, y: y(data[0][i]), width: bw, height: y(0) - y(data[0][i]), rx: 6 }, grp);
if (visible[1]) el('rect', { 'class': 'bar-b', x: cx + 2, y: y(data[1][i]), width: bw, height: y(0) - y(data[1][i]), rx: 6 }, grp);
var tx = el('text', { x: cx, y: H - 8, 'text-anchor': 'middle' }, grp);
tx.textContent = lab;
points.push([cx, y(data[2][i])]);
});
if (visible[2]) {
var d = points.map(function (p, i) { return (i ? 'L' : 'M') + p[0] + ' ' + p[1]; }).join(' ');
el('path', { d: d, 'class': 'trend' });
points.forEach(function (p) { el('circle', { cx: p[0], cy: p[1], r: 4.5, 'class': 'trend-dot' }); });
}
labels.forEach(function (lab, i) {
var hit = el('rect', { 'class': 'hit', x: m.l + step * i, y: m.t, width: step, height: ih, tabindex: 0, role: 'img', 'aria-label': lab + ': ' + names.map(function (n, s) { return n + ' ' + data[s][i]; }).join(', ') });
var show = function () { showTip(i, step); };
hit.addEventListener('mouseenter', show);
hit.addEventListener('focus', show);
hit.addEventListener('mousemove', show);
hit.addEventListener('mouseleave', hideTip);
hit.addEventListener('blur', hideTip);
});
}
function showTip(i, step) {
var rect = svg.getBoundingClientRect();
var scale = rect.width / W;
var cx = (m.l + step * i + step / 2) * scale;
var rows = names.map(function (n, s) { return visible[s] ? n + ': <b style="display:inline">' + data[s][i] + 'k</b>' : ''; }).filter(Boolean).join('<br>');
tip.innerHTML = '<b>' + labels[i] + '</b>' + rows;
tip.classList.add('show');
var tw = tip.offsetWidth / 2;
tip.style.left = Math.max(tw, Math.min(rect.width - tw, cx)) + 'px';
tip.style.top = '24px';
tip.style.transform = 'translate(-50%, 0)';
}
function hideTip() { tip.classList.remove('show'); }
function initLegend() {
var box = document.getElementById('chartLegend');
if (!box) return;
box.addEventListener('click', function (e) {
var btn = e.target.closest('.legend-btn');
if (!btn) return;
var s = +btn.dataset.series;
visible[s] = !visible[s];
btn.classList.toggle('active', visible[s]);
btn.setAttribute('aria-pressed', visible[s]);
draw();
});
}
draw();
initLegend();
var lastW = wrap.clientWidth;
window.addEventListener('resize', function () {
if (wrap.clientWidth !== lastW) { lastW = wrap.clientWidth; draw(); }
});
})();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.