Dumbbell Chart — Free Before After Comparison JS Snippet

Dumbbell Chart · Charts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Two-point comparison
A dot per series with a connector showing the gap.
Direction-aware bar
Connector spans from the lower to the higher value automatically.
Animated draw-on
Double-rAF guarantees the transition runs, not snaps.
Overlap handling
z-index keeps the after dot above the before dot.
Native tooltips
Each dot shows its exact value on hover.
CSS-only positioning
Percent-based layout, no SVG or canvas.
Data-driven
Built from a { name, a, b } array and one MAX.
No dependency
Pure HTML/CSS/JS — no charting library.

About this UI Snippet

Dumbbell Chart — Two-Point Before / After Comparison

Screenshot of the Dumbbell Chart snippet rendered live

A dumbbell chart (also called a DNA or connected-dot plot) compares two values for each category — before and after, two years, two groups — by placing a dot for each on a shared axis and joining them with a line. The length and direction of the connector make the change between the two points obvious at a glance, which is exactly what a side-by-side pair of bars hides. This snippet builds one in plain HTML, CSS, and vanilla JavaScript, no SVG and no charting library.

Two dots and a connector from three numbers

Each row is a CSS grid of a label and a track. Inside the track sit a thin connector .db-bar and two .db-dots. The dots are absolutely positioned at value / max * 100%, and the connector is positioned at the *lower* of the two values with a width equal to the gap between them — left: pos(lo), width: pos(hi) - pos(lo). That single calculation draws the bar exactly between the points regardless of which value is larger.

Encoding the change, not just the values

The reason to choose a dumbbell over grouped bars is that the eye reads the *distance and direction* between the two dots as the change itself. A wide gap means a big shift; the side the higher dot sits on tells you whether it grew or shrank. With grouped bars you'd have to mentally subtract two heights for every category — here the connector does that subtraction visually.

Animated draw-on with the double-rAF trick

On load the connector grows and the dots scale in from zero. Because elements start at their initial state, the code waits two requestAnimationFrames before setting the final positions, guaranteeing the browser has committed the starting styles so the CSS transition actually runs instead of snapping. The dots use a slightly delayed scale so they pop in after the bar settles.

Layering and hit targets

The two dots overlap when values are close, so z-index keeps the "after" dot (b) above the "before" dot (a), and each carries a native title tooltip with its exact value. A :hover brightness bump gives feedback without extra JavaScript.

Data-driven and easy to extend

The chart derives entirely from a { name, a, b } array and a MAX for the axis, so adding categories or swapping the two series is an edit to data. Re-label the legend and axis to fit your comparison — survey waves, A/B test arms, regional figures — and the rendering logic stays identical.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of working out the positioning formula in your head, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the connector bar's left and width are derived from Math.min and Math.max of the two values rather than always from a to b, and why the code waits for two nested requestAnimationFrame calls before setting the final left/width/scale values on the bar and dots. The same assistant can help optimize it, for instance checking whether rebuilding every row's innerHTML from scratch on each data update is necessary or whether only the bar and dot positions need to change for a live-updating dashboard. It's also useful for extending the chart: ask it to add a third comparison point per row (three dots instead of two), sort rows by the size of the gap to surface the biggest movers first, or add a text label showing the numeric delta next to each connector. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a dumbbell (connected dot) comparison chart in plain HTML, CSS, and JavaScript with no SVG and no charting library.

Requirements:
- Render one row per category from a data array of objects each containing a name and two numeric values, using a CSS grid layout with a label column and a track column.
- Inside each row's track, create one thin connector element and two circular dot elements, all absolutely positioned as percentages of a shared maximum value, not pixels, so the layout is responsive to any container width.
- Compute the connector's position and width from the smaller and larger of the two values (not simply the first and second value in that order), so the bar always spans correctly regardless of which of the two values happens to be greater for a given row.
- On load, animate every row's connector growing from zero width and both dots scaling in from zero, but only after two nested requestAnimationFrame calls have elapsed following the initial DOM insertion, so the browser has committed the starting (invisible/zero-width) state before the CSS transition to the final state begins.
- Give the two dots different colors and use z-index to guarantee the second value's dot always renders above the first value's dot when they overlap or are very close together, and give each dot a native title attribute showing its exact value on hover.
- Include a legend identifying which color represents which of the two compared periods or groups, and a shared numeric axis below the rows showing evenly spaced value labels from 0 to the maximum.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA dumbbell chart renders with a legend and a value axis.
  2. 2
    Watch the draw-onConnectors grow and dots scale in from zero on load.
  3. 3
    Read the gapsThe distance between paired dots shows the change per category.
  4. 4
    Hover a dotA tooltip shows the exact value and the dot brightens.
  5. 5
    Edit the dataChange the { name, a, b } array to plot your own pairs.
  6. 6
    Relabel for your caseUpdate the legend and axis for years, groups, or test arms.

Real-world uses

Common Use Cases

Before / after metrics
Show change over time more clearly than a grouped bar chart.
Survey wave comparison
Plot two periods next to a rating breakdown.
A/B test results
Compare arms beside a bullet chart of targets.
Ranking shifts
Pair with a bump chart for rank-over-time.
Regional figures
Contrast two markets alongside a slope chart.
Learning chart design
A reference for connected-dot comparison plots.

Got questions?

Frequently Asked Questions

Use it when the story is the change between two values per category. A dumbbell encodes that change as the distance and direction between two dots, so the eye reads it directly. Grouped bars force the viewer to compare two heights and subtract them mentally for every category, which is slower and easy to misjudge.

The connector is set with left: pos(lo) and width: pos(hi) - pos(lo), where lo and hi are the smaller and larger of the two values. Computing the span from the min and max means the bar always reaches exactly from one dot to the other, no matter which value is higher.

Elements start at their initial CSS state, and a transition only runs if the browser has committed that starting state before the new values are applied. Waiting two animation frames guarantees the start styles are painted first, so setting the final left and width animates smoothly instead of jumping straight to the end.

The dots overlap, so z-index keeps the after dot (b) layered above the before dot (a) and the connector shrinks to a short stub. Each dot still carries its own title tooltip, so you can hover to read both exact values even when they sit on top of each other.

Map your data array to rows in the template and compute each dot's left and the connector's left/width as percentages of the max. Trigger the draw-on by setting the final positions in a mount effect (useEffect, onMounted, ngAfterViewInit). In Tailwind, position the dots and bar with inline percentage styles and use utilities for colors and spacing.