# Bar Chart

> Responsive vertical bar chart with gridlines, hover tooltips, mount animation and a hidden accessible data table.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/bar-chart
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/bar-chart.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/bar-chart-demo.tsx"
import { BarChart } from "@/components/bar-chart"

const revenue = [
  { label: "Jan", value: 18400 },
  { label: "Feb", value: 22100 },
  { label: "Mar", value: 19800 },
  { label: "Apr", value: 27300 },
  { label: "May", value: 31500 },
  { label: "Jun", value: 29200 },
  { label: "Jul", value: 36800 },
]

const signups = [
  { label: "Mon", value: 42 },
  { label: "Tue", value: 58 },
  { label: "Wed", value: 71 },
  { label: "Thu", value: 64 },
  { label: "Fri", value: 89 },
]

export default function BarChartDemo() {
  return (
    <div className="grid w-full max-w-xl grid-cols-[minmax(0,1fr)] gap-6">
      <div className="rounded-2xl border bg-card p-5 shadow-sm">
        <p className="text-sm text-muted-foreground">Monthly revenue</p>
        <p className="mb-5 text-2xl font-semibold tabular-nums">$36,800</p>
        <BarChart data={revenue} title="Monthly revenue" valuePrefix="$" tone="primary" />
      </div>
      <div className="rounded-2xl border bg-card p-5 shadow-sm">
        <p className="mb-5 text-sm text-muted-foreground">New sign-ups this week</p>
        <BarChart data={signups} title="Sign-ups this week" tone="emerald" height={160} />
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/bar-chart.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

export interface BarChartDatum {
  label: string
  value: number
}

export type BarChartTone = "primary" | "emerald" | "sky" | "violet" | "amber" | "rose"

export interface BarChartProps extends Omit<React.ComponentProps<"div">, "children"> {
  data: BarChartDatum[]
  /** Plot area height in px */
  height?: number
  tone?: BarChartTone
  /** Accessible title used in the summary */
  title?: string
  valuePrefix?: string
  valueSuffix?: string
}

const barTone: Record<BarChartTone, string> = {
  primary: "bg-primary",
  emerald: "bg-emerald-500 dark:bg-emerald-400",
  sky: "bg-sky-500 dark:bg-sky-400",
  violet: "bg-violet-500 dark:bg-violet-400",
  amber: "bg-amber-500 dark:bg-amber-400",
  rose: "bg-rose-500 dark:bg-rose-400",
}

const TICKS = 4
const full = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 })
const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 })

function niceMax(value: number) {
  if (value <= 0) return 1
  const exp = 10 ** Math.floor(Math.log10(value))
  const f = value / exp
  const nice = [1, 2, 4, 6, 8, 10].find((n) => f <= n) ?? 10
  return nice * exp
}

const keyframes = `@keyframes atelier-bar-grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}
@media (prefers-reduced-motion:reduce){.atelier-bar{animation:none!important}}`

export function BarChart({
  data,
  height = 220,
  tone = "primary",
  title = "Bar chart",
  valuePrefix = "",
  valueSuffix = "",
  className,
  ...props
}: BarChartProps) {
  const [hovered, setHovered] = React.useState<number | null>(null)
  const max = niceMax(Math.max(0, ...data.map((d) => d.value)))
  const ticks = Array.from({ length: TICKS + 1 }, (_, i) => (max / TICKS) * i)
  const fmt = (v: number) => `${valuePrefix}${full.format(v)}${valueSuffix}`
  const summary = `${title}: ${data.map((d) => `${d.label} ${fmt(d.value)}`).join(", ")}`

  return (
    <div className={cn("w-full", className)} {...props}>
      <style>{keyframes}</style>
      <div role="img" aria-label={summary}>
        <div className="flex" style={{ height }}>
          <div className="relative w-10 shrink-0" aria-hidden>
            {ticks.map((t) => (
              <span
                key={t}
                className="absolute right-2 translate-y-1/2 text-[11px] leading-none text-muted-foreground tabular-nums"
                style={{ bottom: `${(t / max) * 100}%` }}
              >
                {valuePrefix}
                {compact.format(t)}
                {valueSuffix}
              </span>
            ))}
          </div>
          <div className="relative flex-1">
            {ticks.map((t) => (
              <div
                key={t}
                aria-hidden
                className={cn("absolute inset-x-0 border-t", t === 0 ? "border-border" : "border-dashed border-border/70")}
                style={{ bottom: `${(t / max) * 100}%` }}
              />
            ))}
            <div className="absolute inset-0 flex items-end gap-1 px-1 sm:gap-2 sm:px-2">
              {data.map((d, i) => {
                const pct = (Math.max(d.value, 0) / max) * 100
                const active = hovered === i
                return (
                  <div
                    key={d.label}
                    className="relative flex h-full flex-1 items-end justify-center"
                    onPointerEnter={() => setHovered(i)}
                    onPointerLeave={() => setHovered((cur) => (cur === i ? null : cur))}
                  >
                    <div
                      className={cn(
                        "atelier-bar w-full max-w-12 origin-bottom rounded-t-md transition-[opacity,filter] duration-200",
                        barTone[tone],
                        hovered !== null && !active && "opacity-50"
                      )}
                      style={{
                        height: `${pct}%`,
                        animation: `atelier-bar-grow 650ms cubic-bezier(0.2, 0.8, 0.2, 1) ${i * 60}ms both`,
                      }}
                    />
                    {active && (
                      <span
                        className="pointer-events-none absolute left-1/2 z-10 -translate-x-1/2 rounded-md bg-foreground px-2 py-1 text-xs font-medium whitespace-nowrap text-background shadow-md tabular-nums"
                        style={{ bottom: `calc(${pct}% + 8px)` }}
                      >
                        {d.label}: {fmt(d.value)}
                      </span>
                    )}
                  </div>
                )
              })}
            </div>
          </div>
        </div>
        <div className="mt-2 pl-10" aria-hidden>
          <div className="flex gap-1 px-1 sm:gap-2 sm:px-2">
            {data.map((d) => (
              <span key={d.label} className="min-w-0 flex-1 truncate text-center text-xs text-muted-foreground">
                {d.label}
              </span>
            ))}
          </div>
        </div>
      </div>
      <table className="sr-only">
        <caption>{title}</caption>
        <thead>
          <tr>
            <th scope="col">Label</th>
            <th scope="col">Value</th>
          </tr>
        </thead>
        <tbody>
          {data.map((d) => (
            <tr key={d.label}>
              <th scope="row">{d.label}</th>
              <td>{fmt(d.value)}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}
```
