shadimage
dashboardcn
uibar-chartsource: ok · hit

Bar Chart

A single-series bar chart with gradient or striped fills, highlighted bars, and a hover marker.

npx shadcn add "https://dashboardcn.com/r/bar-chart.json"
free

Visual style

gradient

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
chartsrecharts

Registry dependencies

chart

Files1 · source cached 10/15/2026, 4:10:58 AM

registry/dashboardcn/ui/bar-chart.tsx
"use client"

import * as React from "react"
import {
  Bar,
  BarChart as RechartsBarChart,
  CartesianGrid,
  Cell,
  Rectangle,
  ReferenceLine,
  XAxis,
  YAxis,
} from "recharts"

import { cn } from "@/lib/utils"
import { ChartContainer, ChartTooltip } from "@/components/ui/chart"

export type BarRow = Record<string, unknown>

export interface BarReferenceLine {
  /** Horizontal line at this y value. */
  y: number
  label?: string
  /** Any CSS color. Defaults to the muted foreground. */
  color?: string
  /** Defaults to dashed. */
  dashed?: boolean
}

export interface BarChartProps
  extends Omit<React.ComponentProps<typeof ChartContainer>, "config" | "children"> {
  data: BarRow[]
  /** Key of the x-axis value in each row. */
  xKey: string
  /** Key of the bar value in each row. */
  yKey: string
  /**
   * Key of a coarser label, e.g. "month" for weekly rows. When set, the x-axis
   * shows one tick per group centered under its bars instead of a tick per bar.
   */
  groupKey?: string
  /** "gradient" fades each bar toward the bottom; "striped" fills it with diagonal bands. */
  variant?: "gradient" | "striped" | "solid"
  /** Any CSS color for highlighted bars. Defaults to chart-1. */
  color?: string
  /** Any CSS color for bars that are not highlighted. Defaults to the muted foreground. */
  mutedColor?: string
  /** Return true for bars drawn in color. Every bar is highlighted when omitted. */
  highlight?: (row: BarRow, index: number) => boolean
  grid?: "dashed" | "solid" | "none"
  showYAxis?: boolean
  showTooltip?: boolean
  /** Draw a hollow ring on top of the hovered bar. */
  showActiveMarker?: boolean
  barRadius?: number
  /** Animate bars after hydration and when data changes. Respects reduced motion. */
  animate?: boolean
  animationDuration?: number
  /** Horizontal lines, e.g. a goal or an average. The y-axis extends to fit them. */
  referenceLines?: BarReferenceLine[]
  xFormatter?: (value: unknown) => string
  yFormatter?: (value: number) => string
  /** Secondary line of the tooltip. Defaults to the formatted x value. */
  tooltipLabel?: (row: BarRow, index: number) => React.ReactNode
  onBarClick?: (row: BarRow, index: number) => void
}

const reducedMotionQuery = "(prefers-reduced-motion: reduce)"

function subscribeToReducedMotion(onChange: () => void) {
  const query = window.matchMedia(reducedMotionQuery)
  query.addEventListener("change", onChange)
  return () => query.removeEventListener("change", onChange)
}

function getReducedMotionSnapshot() {
  return window.matchMedia(reducedMotionQuery).matches
}

function getReducedMotionServerSnapshot() {
  return true
}

function defaultXFormatter(value: unknown) {
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
    const [y, m, d] = value.split("-").map(Number)
    return new Date(y!, m! - 1, d).toLocaleDateString("en-US", {
      month: "short",
      day: "numeric",
    })
  }
  return String(value)
}

/** One tick per group, placed under the middle bar of the group. */
function groupTicks(data: BarRow[], groupKey: string) {
  const labels: string[] = Array.from({ length: data.length }, () => "")
  let start = 0
  for (let i = 1; i <= data.length; i++) {
    if (i === data.length || data[i]![groupKey] !== data[start]![groupKey]) {
      labels[Math.floor((start + i - 1) / 2)] = String(data[start]![groupKey] ?? "")
      start = i
    }
  }
  return labels
}

interface ShapeProps {
  x?: number
  y?: number
  width?: number
  height?: number
  index?: number
}

/** A single-series bar chart with gradient or striped fills, highlighted bars, and a hover marker. */
function BarChart({
  data,
  xKey,
  yKey,
  groupKey,
  variant = "gradient",
  color = "var(--chart-1)",
  mutedColor = "var(--muted-foreground)",
  highlight,
  grid = "dashed",
  showYAxis = false,
  showTooltip = true,
  showActiveMarker = true,
  barRadius = 6,
  animate = true,
  animationDuration = 500,
  referenceLines = [],
  xFormatter = defaultXFormatter,
  yFormatter,
  tooltipLabel,
  onBarClick,
  className,
  ...props
}: BarChartProps) {
  const id = React.useId()
  const reducedMotion = React.useSyncExternalStore(
    subscribeToReducedMotion,
    getReducedMotionSnapshot,
    getReducedMotionServerSnapshot
  )
  const isHighlighted = React.useCallback(
    (index: number) => (highlight ? highlight(data[index]!, index) : true),
    [data, highlight]
  )
  const tickLabels = React.useMemo(
    () => (groupKey ? groupTicks(data, groupKey) : null),
    [data, groupKey]
  )
  const fillFor = (index: number) =>
    `url(#${id}-${isHighlighted(index) ? "on" : "off"})`

  const activeBar = (shape: ShapeProps) => {
    const { x = 0, y = 0, width = 0, height = 0, index = 0 } = shape
    const on = isHighlighted(index)
    return (
      <g>
        <Rectangle {...shape} fill={fillFor(index)} />
        {showActiveMarker && height > 0 ? (
          <circle
            cx={x + width / 2}
            cy={y}
            r={4}
            fill="var(--background)"
            stroke={on ? color : mutedColor}
            strokeWidth={2}
          />
        ) : null}
      </g>
    )
  }

  return (
    <ChartContainer
      data-slot="bar-chart"
      config={{ [yKey]: { label: yKey, color } }}
      className={cn("aspect-auto h-64 w-full", className)}
      {...props}
    >
      <RechartsBarChart
        data={data}
        margin={{ top: 12, right: 8, bottom: 0, left: showYAxis ? 0 : 8 }}
        barCategoryGap="28%"
      >
        <defs>
          {variant === "gradient" ? (
            <>
              <linearGradient id={`${id}-on`} x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stopColor={color} stopOpacity={1} />
                <stop offset="100%" stopColor={color} stopOpacity={0.6} />
              </linearGradient>
              <linearGradient id={`${id}-off`} x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stopColor={mutedColor} stopOpacity={0.35} />
                <stop offset="100%" stopColor={mutedColor} stopOpacity={0.18} />
              </linearGradient>
            </>
          ) : variant === "striped" ? (
            <>
              {(["on", "off"] as const).map((state) => {
                const fill = state === "on" ? color : mutedColor
                const opacity = state === "on" ? 1 : 0.3
                return (
                  <pattern
                    key={state}
                    id={`${id}-${state}`}
                    patternUnits="userSpaceOnUse"
                    width={10}
                    height={10}
                    patternTransform="rotate(45)"
                  >
                    <rect width={10} height={10} fill={fill} fillOpacity={opacity} />
                    <rect
                      width={5}
                      height={10}
                      fill="var(--background)"
                      fillOpacity={0.25}
                    />
                  </pattern>
                )
              })}
            </>
          ) : (
            <>
              <linearGradient id={`${id}-on`}>
                <stop stopColor={color} />
              </linearGradient>
              <linearGradient id={`${id}-off`}>
                <stop stopColor={mutedColor} stopOpacity={0.3} />
              </linearGradient>
            </>
          )}
        </defs>
        {grid !== "none" ? (
          <CartesianGrid
            vertical={false}
            strokeDasharray={grid === "dashed" ? "6 6" : undefined}
          />
        ) : null}
        <XAxis
          dataKey={xKey}
          tickLine={false}
          axisLine={false}
          tickMargin={10}
          interval={tickLabels ? 0 : "preserveStartEnd"}
          minTickGap={tickLabels ? 0 : 24}
          tickFormatter={
            tickLabels ? (_, index) => tickLabels[index] ?? "" : (v) => xFormatter(v)
          }
        />
        <YAxis
          hide={!showYAxis}
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          width={44}
          tickFormatter={yFormatter}
        />
        {showTooltip ? (
          <ChartTooltip
            cursor={false}
            isAnimationActive={false}
            content={({ active, payload }) => {
              const entry = payload?.[0]
              if (!active || !entry) return null
              const row = entry.payload as BarRow
              const index = data.indexOf(row)
              const value = Number(row[yKey])
              return (
                <div className="bg-background rounded-lg border px-3 py-2 text-xs shadow-md">
                  <div className="text-sm font-semibold tabular-nums">
                    {yFormatter ? yFormatter(value) : String(value)}
                  </div>
                  <div className="text-muted-foreground">
                    {tooltipLabel ? tooltipLabel(row, index) : xFormatter(row[xKey])}
                  </div>
                </div>
              )
            }}
          />
        ) : null}
        <Bar
          dataKey={yKey}
          radius={barRadius}
          isAnimationActive={animate && !reducedMotion}
          animationDuration={animationDuration}
          animationEasing="ease-out"
          activeBar={activeBar}
          onClick={
            onBarClick
              ? (_, index) => onBarClick(data[index]!, index)
              : undefined
          }
          className={onBarClick ? "cursor-pointer" : undefined}
        >
          {data.map((_, index) => (
            <Cell key={index} fill={fillFor(index)} />
          ))}
        </Bar>
        {referenceLines.map((line, index) => (
          <ReferenceLine
            key={index}
            y={line.y}
            ifOverflow="extendDomain"
            stroke={line.color ?? "var(--muted-foreground)"}
            strokeDasharray={line.dashed === false ? undefined : "4 4"}
            label={
              line.label
                ? {
                    value: line.label,
                    position: "insideTopRight",
                    fill: line.color ?? "var(--muted-foreground)",
                    fontSize: 11,
                  }
                : undefined
            }
          />
        ))}
      </RechartsBarChart>
    </ChartContainer>
  )
}

export { BarChart }

More from dashboardcn

Browse all components in this library →