Bar Chart Race
Horizontal bar ranking: when a value changes, the bars stretch and the rows slide to their new rank.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Data has 28 effects, including 1 free. Explore the category →
3 usage examples



How it works
The ranking is a real ordered list <ol class="rce-list">: each <li class="rce-item" data-value="…"> holds its label. The script adds a .rce-track with its .rce-bar and the .rce-val number to every row. The largest value fills the whole track and the others are proportional (or relative to data-max if you set it).
When a data-value changes, the script records each row's position, puts the rows back in descending order in the DOM, then starts them from their old place with a translateY that it brings back to zero: the FLIP technique. Rows therefore glide to their new rank while the bar widths change (0.8 s transition, cubic-bezier(.23, 1, .32, 1) curve). Several values changed at once give a single reorder.
At start-up every row gets one of five gradients (amber, blue, red, cyan, violet) based on its original position and keeps it while it climbs or drops. The first time the chart scrolls into view, the bars grow from zero.
Labels are truncated with an ellipsis when wider than their column (--rce-label-width); values are formatted in the page language with data-unit.
Accessibility
- Reading order matches display order: rows are really moved in the DOM (no
orderproperty), so a screen reader goes through the list top to bottom as it is shown. Each row reads "label, value". - List name: the
aria-labelon.rce-list("Ranking") says what is ranked; adapt it to your data. - Reduced motion: under
prefers-reduced-motion: reduce, bars and rows take their final size and place without sliding, on first display and on updates. - No automatic announcement: a change of rank is not spoken. If an overtake matters (new leader), announce it in a separate sentence.
Browser compatibility
CSS Grid, width and transform transitions, MutationObserver, IntersectionObserver and Intl.NumberFormat. Measured in Chromium 151, Firefox 153 and WebKit 26.5: 150 ms after a change, rows sit between their old and new place in all three engines; two independent rankings on one page, 390 px, white background, no JS error.
Without JavaScript the ordered list and its labels stay readable, with no bars or values: put the value in the label text if it must remain visible without script.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0245-root">
<ol class="rce-list" aria-label="Ranking">
<li class="rce-item" data-value="70"><span class="rce-label">Amber</span></li>
<li class="rce-item" data-value="60"><span class="rce-label">Cobalt</span></li>
<li class="rce-item" data-value="45"><span class="rce-label">Coral</span></li>
<li class="rce-item" data-value="40"><span class="rce-label">Lagoon</span></li>
<li class="rce-item" data-value="35"><span class="rce-label">Lilac</span></li>
</ol>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
data-value on each .rce-item (HTML) |
70, 60, 45, 40, 35 | Row value. Changing it re-runs the bar stretch and the re-ranking. |
data-max on the root (HTML) |
the largest value | Value matching a full bar; handy for a fixed scale (e.g. 100). |
data-unit, data-decimals (HTML) |
none, 0 | Unit appended to each value ("42 pts") and decimals. |
--rce-color (CSS, on one row or for the whole ranking) |
5 rotating gradients | Bar colour. Set on a row (style="--rce-color:…"), it follows that row wherever it moves. For the whole ranking, use a rule placed after the effect's CSS and more specific than .rce-item alone, for example .my-ranking .rce-item: otherwise the five default tints win. |
--rce-bar-height, --rce-gap (CSS) |
24px, 8px | Bar thickness and space between rows. |
--rce-label-width, --rce-max-width (CSS) |
4.5em, 420px | Width of the label column and maximum width of the ranking. |
--rce-text, --rce-value (CSS) |
70% white, 60% white | Label and value colours; darken them on a light background. |
--rce-speed (CSS) |
.8s | Duration of the bar and row slides. |
--rce-bg, --rce-min-height (CSS) |
#0a0a0f, 300px | Root background and minimum height. |
FAQ
rows.forEach((li, i) => li.setAttribute('data-value', scores[i])). The script waits for the block to finish before re-ranking, so rows only move once, all together.--rce-bar-height and --rce-gap, and keep in mind that ten rows moving at once becomes hard to follow.