Slope Chart — HTML CSS JS Slopegraph (No Library)
Slope Chart · Charts · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="sl-card">
<div class="sl-head"><h3>Market share: 2024 → 2025</h3></div>
<div class="sl-cols"><span>2024</span><span>2025</span></div>
<svg class="sl-svg" id="slSvg" viewBox="0 0 380 280" role="img" aria-label="Slope chart"></svg>
<div class="sl-tip" id="slTip" hidden></div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.sl-card{position:relative;background:#fff;border-radius:16px;padding:22px;width:100%;max-width:440px;box-shadow:0 18px 44px rgba(15,23,42,.08)}
.sl-head h3{font-size:16px;font-weight:800;color:#0f172a;margin-bottom:12px}
.sl-cols{display:flex;justify-content:space-between;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;color:#94a3b8;padding:0 8px}
.sl-svg{width:100%;height:auto;display:block}
.sl-line{stroke-width:2;transition:stroke-width .12s,opacity .12s}
.sl-dot{transition:r .12s}
.sl-row:hover .sl-line{stroke-width:3.5}
.sl-row:hover .sl-dot{r:6}
.sl-row.dim{opacity:.28}
.sl-lab{font-size:11px;font-weight:700;fill:#334155}
.sl-val{font-size:11px;font-weight:800;fill:#0f172a}
.sl-tip{position:absolute;pointer-events:none;background:#0f172a;color:#fff;font-size:12px;font-weight:700;padding:6px 10px;border-radius:8px;transform:translate(-50%,-130%);white-space:nowrap;z-index:5}
.sl-tip[hidden]{display:none}// Each series has a 'before' and 'after' value; the slope shows the change.
var DATA = [
{ name: 'Acme', before: 34, after: 41, color: '#6366f1' },
{ name: 'Globex', before: 28, after: 22, color: '#ec4899' },
{ name: 'Initech', before: 18, after: 24, color: '#22c55e' },
{ name: 'Umbrella', before: 12, after: 9, color: '#f59e0b' },
{ name: 'Stark', before: 8, after: 4, color: '#0ea5e9' },
];
var svg = document.getElementById('slSvg');
var tip = document.getElementById('slTip');
var card = document.querySelector('.sl-card');
var SVGNS = 'http://www.w3.org/2000/svg';
var W = 380, H = 280, PADY = 22, LX = 96, RX = W - 96;
function el(n, a) { var e = document.createElementNS(SVGNS, n); for (var k in a) e.setAttribute(k, a[k]); return e; }
function render() {
var all = DATA.reduce(function (a, d) { return a.concat([d.before, d.after]); }, []);
var max = Math.max.apply(null, all), min = Math.min.apply(null, all);
function y(v) { return (H - PADY) - ((v - min) / (max - min)) * (H - PADY * 2); }
svg.innerHTML = '';
// Two vertical axis lines.
svg.appendChild(el('line', { x1: LX, y1: PADY, x2: LX, y2: H - PADY, stroke: '#e2e8f0', 'stroke-width': 1.5 }));
svg.appendChild(el('line', { x1: RX, y1: PADY, x2: RX, y2: H - PADY, stroke: '#e2e8f0', 'stroke-width': 1.5 }));
DATA.forEach(function (d, i) {
var g = el('g', { class: 'sl-row' });
g.dataset.i = i;
var y1 = y(d.before), y2 = y(d.after);
g.appendChild(el('line', { class: 'sl-line', x1: LX, y1: y1, x2: RX, y2: y2, stroke: d.color }));
g.appendChild(el('circle', { class: 'sl-dot', cx: LX, cy: y1, r: 4, fill: d.color }));
g.appendChild(el('circle', { class: 'sl-dot', cx: RX, cy: y2, r: 4, fill: d.color }));
// Left label: name + before value. Right label: after value + name.
var lname = el('text', { class: 'sl-lab', x: LX - 12, y: y1 + 4, 'text-anchor': 'end' }); lname.textContent = d.name; g.appendChild(lname);
var lval = el('text', { class: 'sl-val', x: LX - 12, y: y1 - 9, 'text-anchor': 'end' }); lval.textContent = d.before + '%'; g.appendChild(lval);
var rval = el('text', { class: 'sl-val', x: RX + 12, y: y2 - 9, 'text-anchor': 'start' }); rval.textContent = d.after + '%'; g.appendChild(rval);
var rname = el('text', { class: 'sl-lab', x: RX + 12, y: y2 + 4, 'text-anchor': 'start' }); rname.textContent = d.name; g.appendChild(rname);
svg.appendChild(g);
});
}
svg.addEventListener('mousemove', function (e) {
var row = e.target.closest('.sl-row');
svg.querySelectorAll('.sl-row').forEach(function (r) { r.classList.toggle('dim', !!row && r !== row); });
if (!row) { tip.hidden = true; return; }
var d = DATA[+row.dataset.i];
var change = d.after - d.before;
var rect = card.getBoundingClientRect();
tip.textContent = d.name + ': ' + (change >= 0 ? '+' : '') + change + ' pts';
tip.style.left = (e.clientX - rect.left) + 'px';
tip.style.top = (e.clientY - rect.top) + 'px';
tip.hidden = false;
});
svg.addEventListener('mouseleave', function () {
svg.querySelectorAll('.sl-row').forEach(function (r) { r.classList.remove('dim'); });
tip.hidden = true;
});
render();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Slope Chart — Before-and-After Lines That Show Change at a Glance

A slope chart (or slopegraph, popularised by Edward Tufte) compares two points in time by drawing a line for each category between its before and after value on two parallel axes. The slope of each line — up, down, steep, flat — instantly communicates the direction and size of change, and crossings show rank shifts. This snippet builds it in plain HTML, CSS, SVG, and vanilla JavaScript, with labelled endpoints and hover focus — no charting library.
Two axes, one line per category
The chart has two vertical axes — left for the "before" period, right for "after" — and each category is a single line connecting its two values, with a dot at each end. A line that climbs rose; one that falls dropped; a steep line changed a lot. This direct encoding of change as slope is what a slopegraph does better than a grouped bar chart or two pies: the eye reads the trend without comparing bar heights across a gap.
A shared scale for honest slopes
Both axes share one scale computed from the combined min and max of all values, so a slope's steepness genuinely reflects the magnitude of change and lines are comparable across categories. The y() function maps each value to a pixel position on that shared range (inverted for SVG). Using one scale for both columns is essential — separate scales would make the slopes meaningless.
Endpoints labelled on both sides
Each line is labelled at both ends: the category name and its before value on the left, its after value and name on the right. Labelling both endpoints (rather than relying on a legend) lets the reader follow any line across without losing track of which is which — the convention that makes a slopegraph readable even with crossing lines.
Hover to focus
Hovering a line dims all the others and thickens the focused one, with a tooltip showing the exact change in points. This focus-and-dim interaction is what tames a busy slope chart: when several lines cross, hovering isolates the one you care about so you can trace it cleanly. The whole row (line, dots, labels) is one SVG group, so it highlights together.
Data-driven and drop-in
It renders from a DATA array of { name, before, after, color }. Swap in any two-period comparison — market share, rankings, prices, survey results across two years — and the slopes, scale, and labels follow. It is a clear, dependency-free reference for slopegraph construction: shared-scale endpoints, connecting lines, and focus interaction. A slope chart only scales gracefully to a dozen or so categories before the left and right labels start overlapping vertically — past that point, either label only the lines a viewer is most likely to care about (the biggest movers) and let the rest go unlabeled until hovered, or switch to small multiples grouping related categories into separate, less crowded slope charts.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA slope chart renders connecting each company's 2024 and 2025 market share.
- 2Read the slopesUpward lines rose, downward fell; steeper means a bigger change, crossings mean rank shifts.
- 3Hover a lineThe others dim and a tooltip shows the change in points for the focused line.
- 4Swap in your dataReplace the DATA array with your own { name, before, after, color } items.
- 5Use for any two periodsYears, before/after, baseline/result — any two comparable points work.
- 6Wire to an APIMap your two-period data into the DATA shape and call render().
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Comparing exactly two points — usually two time periods — across several categories. Each category becomes a line whose slope shows the direction and size of change, and crossings reveal rank swaps. It communicates change more directly than grouped bars (no comparing heights across a gap) or two pies (no tracking slices between them), which is why Tufte popularised it for before/after comparisons.
The whole point of a slope chart is that the steepness of a line reflects how much a value changed. If each axis had its own scale, identical changes would render as different slopes and the chart would mislead. The snippet computes one min/max from all values across both periods and maps both endpoints through the same y() function, so every slope is comparable and honest.
When several lines are close together or cross, a legend forces the reader to match colours back and forth. Labelling each line's name and value at both endpoints lets the eye follow any line straight across the chart without losing it. This dual labelling is the standard slopegraph convention and is what keeps it readable as the number of categories grows.
With many categories the lines can overlap and cross, making one hard to trace. Hovering a line adds a dim class to all the other rows and thickens the focused line and its dots, visually isolating it, while a tooltip shows its exact change. Because each line, its dots, and labels are grouped in one SVG <g>, they highlight and dim together.
In React, hold the data in useState and render the line groups from .map() (or run render() in a useEffect with a ref), tracking the hovered index in state for the dim effect; in Vue, use v-for with a hovered ref; in Angular, *ngFor with a property. The shared-scale y() math is framework-agnostic and ports unchanged.