Bar Chart Grow

Bars rise from the baseline one after another as the chart scrolls into view, with each value appearing on top.

  • CSS
  • SVG & shapes
  • Scroll
  • Added
Orders per day

AI prompt

Create a React bar chart whose bars grow when it scrolls into view. Render 7 bars for Mon to Sun as divs (or SVG rects) with height set from the data as a percentage of the max value, and transform-origin bottom. Start every bar at transform: scaleY(0); when an IntersectionObserver (threshold 0.5, once) fires, set data-in on the chart and transition each bar to scaleY(1) over 700ms with cubic-bezier(0.16, 1, 0.3, 1), delayed by index × 70ms. Fade each value label in 300ms after its bar. Highlight the highest bar with the accent colour. Keep the data readable without the animation: a visually hidden table or aria-label on each bar with the day and value. Title: "Orders per day". Add a "Replay" button. Under prefers-reduced-motion, show the bars at full height immediately.

Source

Demo.tsx
'use client'

import { useEffect, useRef, useState } from 'react'
import s from './demo.module.css'

const DATA = [42, 58, 51, 73, 96, 64, 38]

export default function BarChartGrow({ labels }: { labels: Record<string, string> }) {
  const days = labels.days.split('|')
  const max = Math.max(...DATA)
  const ref = useRef<HTMLDivElement>(null)
  const [run, setRun] = useState(0)

  useEffect(() => {
    const el = ref.current
    if (!el) return
    const io = new IntersectionObserver(
      ([e]) => {
        if (!e.isIntersecting) return
        el.dataset.in = ''
        io.disconnect()
      },
      { threshold: 0.5 },
    )
    io.observe(el)
    return () => io.disconnect()
  }, [run])

  return (
    <div className={s.wrap}>
      <div className={s.head}>
        <span className={s.title}>{labels.title}</span>
        <button type="button" className={s.button} onClick={() => setRun(run + 1)}>
          {labels.replay}
        </button>
      </div>
      <div key={run} ref={ref} className={s.chart} role="list">
        {DATA.map((v, i) => (
          <div key={days[i]} className={s.col} role="listitem" aria-label={`${days[i]}: ${v}`} style={{ '--i': i } as React.CSSProperties}>
            <span className={s.value} aria-hidden="true">
              {v}
            </span>
            <span className={s.bar} data-max={v === max || undefined} style={{ height: `calc((100% - 34px) * ${v / max})` }} />
            <span className={s.day} aria-hidden="true">
              {days[i]}
            </span>
          </div>
        ))}
      </div>
    </div>
  )
}
demo.module.css
.wrap { display: grid; gap: 12px; width: min(300px, 100%); }

.head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.title { font: 600 var(--text-sm) / 1 var(--font-body); color: var(--stage-ink); }

.chart {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 8px;
  height: 140px;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--stage-rule);
}

.col { display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 4px; min-width: 0; }

.bar {
  width: 100%;
  border-radius: 4px 4px 0 0;
  background: var(--stage-muted);
  transform: scaleY(0);
  transform-origin: bottom;
}

.bar[data-max] { background: var(--stage-accent); }

.value, .day {
  font: 500 0.625rem / 1 var(--font-mono);
  color: var(--stage-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.value { opacity: 0; color: var(--stage-ink); }
.day { margin-top: 4px; }

.chart[data-in] .bar {
  transform: scaleY(1);
  transition: transform 700ms var(--ease-out) calc(var(--i) * 70ms);
}

.chart[data-in] .value {
  opacity: 1;
  transition: opacity 300ms var(--ease-out) calc(var(--i) * 70ms + 300ms);
}

.button {
  height: 28px;
  padding: 0 10px;
  border: 1px solid var(--stage-rule);
  border-radius: 8px;
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-xs) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.button:hover { border-color: var(--stage-accent); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .bar { transform: none; }
  .value { opacity: 1; }
  .chart[data-in] .bar, .chart[data-in] .value { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.