Racing Bar Chart
A horizontal bar chart representing timeseries data where the bars continually sort themselves on the Y-axis as their X-values increase/decrease over time.
Use
Visualizing ranking changes over time (e.g., most popular languages per year)
Avoid
Static data sets
Safer Alternative
No safer default listed.
Risk Level
Low risk
Live Demo
What It Is
A horizontal bar chart representing timeseries data where the bars continually sort themselves on the Y-axis as their X-values increase/decrease over time.
✓When to Use
- Visualizing ranking changes over time (e.g., most popular languages per year)
- Gamified leaderboards
✕When NOT to Use
- Static data sets
Configuration Tips
- 01Use FLIP (First, Last, Invert, Play) animation techniques or Framer Motion layout animations to handle the Y-axis sorting smoothly
You've Seen It In
AI Implementation Prompts
Move from a fast scaffold to production details, then tune timing and edge states.
Create a racing bar chart where items physically swap vertical positions as their values overtake each other.
Reference Implementation
The same implementation approach used by the live preview, kept compact enough to inspect and adapt.
"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const initialData = [
{ id: '1', label: 'React', color: 'bg-sky-500' },
{ id: '2', label: 'Vue', color: 'bg-emerald-500' },
{ id: '3', label: 'Svelte', color: 'bg-orange-500' },
{ id: '4', label: 'Angular', color: 'bg-red-600' }
];
const frames = [
[
{ id: '1', width: 80, score: 95 },
{ id: '2', width: 60, score: 72 },
{ id: '3', width: 40, score: 48 },
{ id: '4', width: 30, score: 36 }
],
[
{ id: '1', width: 85, score: 98 },
{ id: '2', width: 75, score: 88 },
{ id: '4', width: 60, score: 70 },
{ id: '3', width: 45, score: 52 }
],
[
{ id: '2', width: 95, score: 110 },
{ id: '1', width: 90, score: 105 },
{ id: '3', width: 70, score: 80 },
{ id: '4', width: 65, score: 75 }
],
[
{ id: '2', width: 100, score: 120 },
{ id: '3', width: 90, score: 108 },
{ id: '1', width: 85, score: 106 },
{ id: '4', width: 70, score: 82 }
],
];
export function RacingBarChartDemo({ isPlaying = false }: { isPlaying?: boolean }) {
const [frameIndex, setFrameIndex] = useState(0);
useEffect(() => {
if (!isPlaying) {
setFrameIndex(0);
return;
}
const interval = setInterval(() => {
setFrameIndex((prev) => (prev + 1) % frames.length);
}, 1500);
return () => clearInterval(interval);
}, [isPlaying]);
const currentFrameState = frames[frameIndex].map((frameItem, i) => {
const baseItem = initialData.find(d => d.id === frameItem.id)!;
return {
...baseItem,
width: frameItem.width,
score: frameItem.score,
rank: i "text-stone-400 italic">// Target Y position based on current sorted array index
};
});
return (
<div className="flex h-64 w-full items-center justify-center overflow-hidden rounded-2xl bg-white p-4 shadow-sm ring-1 ring-stone-200">
<div className="w-full max-w-sm h-48 relative border-l border-b border-stone-200 pb-2 pl-2">
<AnimatePresence>
{currentFrameState.map((item) => (
<motion.div
key={item.id}
layout
initial={false}
animate={{
y: item.rank * 42, "text-stone-400 italic">// spacing
width: `${item.width}%`
}}
transition={{
type: "spring",
stiffness: 80,
damping: 20
}}
className={`absolute left-0 h-8 rounded-r-md flex items-center justify-between px-3 min-w-[100px] shadow-sm ${item.color}`}
>
<span className="text-white text-xs font-bold tracking-wider">{item.label}</span>
<span className="text-white/90 text-[10px] font-mono font-bold tabular-nums">{item.score}</span>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
);
}