Bootstrap 5 Animated Gradient Line Chart (No Library)
An animated smooth line chart with a soft gradient fill, a total headline and keyboard-accessible data points with tooltips.
<section class="wdw-stage">
<div class="container" style="max-width:820px">
<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">Growth</span><h2 class="wdw-heading h3 mb-1">New signups</h2><p class="wdw-muted mb-0">January to June, hover or tab to a point for details.</p></div>
<div class="text-md-end"><div class="display-6 wdw-heading lh-1" id="chartTotal">0</div><span class="badge rounded-pill text-bg-success mt-2">+62% vs. January</span></div>
</div>
<div class="gchart" id="gradChart" role="group" aria-label="Line chart of monthly signups"></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 */
.gchart { position: relative; height: 320px; }
.gchart svg { width: 100%; height: 100%; display: block; overflow: visible; }
.gchart .g-grid { stroke: var(--bs-border-color); stroke-dasharray: 3 5; }
.gchart text { fill: var(--bs-secondary-color); font: 500 12px var(--wdw-font); }
.gchart .g-line { fill: none; stroke: var(--bs-primary); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 1; animation: g-draw 1.2s ease forwards; }
.gchart .g-area { opacity: 0; animation: g-fade .8s .5s ease forwards; }
.gchart .g-stop-a { stop-color: var(--bs-primary); stop-opacity: .38; }
.gchart .g-stop-b { stop-color: var(--bs-primary); stop-opacity: 0; }
.gchart .g-guide { stroke: var(--bs-primary); stroke-opacity: .5; opacity: 0; transition: opacity .15s; }
.gchart .g-dot { fill: var(--bs-body-bg); stroke: var(--bs-primary); stroke-width: 3; cursor: pointer; transition: r .15s; outline: none; }
.gchart .g-dot:hover, .gchart .g-dot:focus-visible { r: 8; }
.gchart .g-dot:focus-visible { stroke-width: 4; }
.g-tip { position: absolute; transform: translate(-50%, -135%); pointer-events: none; opacity: 0; transition: opacity .15s; padding: .35rem .75rem; border-radius: .6rem; background: var(--bs-emphasis-color); color: var(--bs-body-bg); font-size: .8rem; font-weight: 700; white-space: nowrap; box-shadow: var(--wdw-shadow-sm); }
.g-tip small { font-weight: 500; opacity: .7; display: block; }
@keyframes g-draw { to { stroke-dashoffset: 0; } }
@keyframes g-fade { to { opacity: 1; } }
@media (max-width: 575.98px) { .gchart { height: 240px; } }
@media (prefers-reduced-motion: reduce) { .gchart .g-line, .gchart .g-area { animation: none; stroke-dashoffset: 0; opacity: 1; } }
function initGradientChart() {
const host = document.getElementById('gradChart');
const totalEl = document.getElementById('chartTotal');
if (!host) return;
const labels = ['January', 'February', 'March', 'April', 'May', 'June'];
const data = [1250, 1840, 3260, 3410, 2020, 2030];
const min = 0, max = 4000, ticks = [0, 1000, 2000, 3000, 4000];
if (totalEl) totalEl.textContent = data.reduce((a, b) => a + b, 0).toLocaleString();
const tip = document.createElement('div');
tip.className = 'g-tip';
const smooth = (pts) => {
let d = 'M' + pts[0];
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
d += 'C' + [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6] + ' ' + [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6] + ' ' + p2;
}
return d;
};
const draw = () => {
const W = host.clientWidth || 700, H = host.clientHeight || 320;
const pl = 40, pr = 16, pt = 16, pb = 30;
const x = (i) => pl + (i / (data.length - 1)) * (W - pl - pr);
const y = (v) => pt + (1 - (v - min) / (max - min)) * (H - pt - pb);
const pts = data.map((v, i) => [Math.round(x(i)), Math.round(y(v))]);
const line = smooth(pts);
let g = '';
ticks.forEach((t) => { g += '<line class="g-grid" x1="' + pl + '" x2="' + (W - pr) + '" y1="' + y(t) + '" y2="' + y(t) + '"/><text x="' + (pl - 10) + '" y="' + (y(t) + 4) + '" text-anchor="end">' + (t / 1000) + 'k</text>'; });
labels.forEach((l, i) => { g += '<text x="' + x(i) + '" y="' + (H - 6) + '" text-anchor="' + (i === 0 ? 'start' : i === labels.length - 1 ? 'end' : 'middle') + '">' + (W < 480 ? l.slice(0, 3) : l) + '</text>'; });
const dots = pts.map((p, i) => '<circle class="g-dot" tabindex="0" data-i="' + i + '" cx="' + p[0] + '" cy="' + p[1] + '" r="5.5" aria-label="' + labels[i] + ': ' + data[i].toLocaleString() + ' signups"/>').join('');
host.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '"><defs><linearGradient id="gcGrad" x1="0" y1="0" x2="0" y2="1"><stop class="g-stop-a" offset="0"/><stop class="g-stop-b" offset="1"/></linearGradient></defs>' + g +
'<line class="g-guide" y1="' + pt + '" y2="' + (H - pb) + '"/>' +
'<path class="g-area" fill="url(#gcGrad)" d="' + line + 'L' + pts[pts.length - 1][0] + ',' + (H - pb) + 'L' + pts[0][0] + ',' + (H - pb) + 'Z"/>' +
'<path class="g-line" pathLength="1" d="' + line + '"/>' + dots + '</svg>';
host.appendChild(tip);
const guide = host.querySelector('.g-guide');
const show = (el) => {
const i = +el.dataset.i;
tip.innerHTML = data[i].toLocaleString() + '<small>' + labels[i] + '</small>';
tip.style.left = pts[i][0] + 'px';
tip.style.top = pts[i][1] + 'px';
tip.style.opacity = 1;
guide.setAttribute('x1', pts[i][0]);
guide.setAttribute('x2', pts[i][0]);
guide.style.opacity = 1;
};
const hide = () => { tip.style.opacity = 0; guide.style.opacity = 0; };
host.querySelectorAll('.g-dot').forEach((d) => {
d.addEventListener('mouseenter', () => show(d));
d.addEventListener('focus', () => show(d));
d.addEventListener('mouseleave', hide);
d.addEventListener('blur', hide);
});
};
draw();
let t;
window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(draw, 120); });
}
initGradientChart();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.