Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/bar-chart.jsonUsage
import { BarChart } from "@/components/dithercn/bar-chart";
const data = [
{ week: "W36", deploys: 61 },
{ week: "W37", deploys: 68 },
{ week: "W38", deploys: 74 },
];
export function Deploys() {
return (
<BarChart
data={data}
x="week"
series={[{ label: "Deploys", value: "deploys" }]}
/>
);
}Each bar is a dithered body under a solid 2px cap. One bar is drawn solid, the last by default, with its value above it. When the pointer or keyboard focus moves to another bar, that one fills in instead. x and each series' value name fields of your data, or take a function when you render from a Client Component.
Examples
Stacked
layout="stacked" puts the series on top of each other with a 2px gap between segments, and the axis covers the totals. highlight="max" picks the tallest stack.
Grouped comparison
Grouped bars sit side by side, one per series. highlight="none" leaves every bar dithered until you scrub to one, and fill="hatched" gives each bar diagonal lines instead of the gradient.
Props
| Prop | Type | Default |
|---|---|---|
| data* One entry per bar or group of bars, in order. | Type: readonly T[] | Default – |
| x* The field (or function) holding each bar's category label. | Type: keyof T | ((datum: T) => string | number) | Default – |
| series* Each series is { label, value, color? }, where value is a numeric field of T or a function. | Type: [ChartSeries<T>, ...ChartSeries<T>[]] | Default – |
| layout How several series share a category: side by side, on top of each other, or stacked and scaled to 100%. | Type: "grouped" | "stacked" | "percent" | Default "grouped" |
| fill gradient is densest at the top of each bar and thins toward the axis. dotted is a sparse lattice, hatched diagonal lines, solid flat colour. | Type: "gradient" | "dotted" | "hatched" | "solid" | Default "gradient" |
| bloom A soft glow in each series' colour behind its fill. Its strength follows --dither-bloom, lower in light mode. | Type: "off" | "low" | "high" | Default "off" |
| colors Colours by series position: palette names such as "green" or any CSS colour. A series' own color wins. | Type: readonly DitherColor[] | Default ink, orange, green, pink, … |
| interactive Scrubbing, the readout and the legend spotlight. false makes a decorative chart: no focus stop, no readout, just an image with a summary. | Type: boolean | Default true |
| animate Entrance motion. Always off under reduced motion. | Type: boolean | Default true |
| highlight The bar drawn solid, with its value, while nothing is scrubbed. | Type: number | "last" | "max" | "none" | Default "last" |
| height Height in px, axes included. | Type: number | Default 240 |
| level Gradient only: coverage at the top of each bar, from 0 to 1. | Type: number | Default 0.75 |
| matrix Gradient only: the dither texture. | Type: "bayer2" | "bayer4" | "bayer8" | "blue-noise" | Default "bayer4" |
| cell Size of one dither cell in px, rounded to whole device pixels. | Type: number | Default --dither-cell |
| axes Show the value and category axes. | Type: boolean | Default true |
| tooltip Show the readout while scrubbing. | Type: boolean | Default true |
| legend Show the legend under the chart. Its entries spotlight a series on hover and lock it on click. | Type: boolean | Default Two or more series |
| format How values are written on the axis, in the readout and in the table. | Type: Intl.NumberFormatOptions | ((value: number) => string) | Default Compact, as in 1.2K |
Series colours follow the palette: ink, then orange, green, pink, purple, blue, red and grey. Pass colors, or color on one series, with a palette name or any CSS colour.
Notes
- The chart is a focusable group with a short summary, and every value is in a visually hidden table. The arrow keys, Home and End move between bars, a polite live region reads out each stop, and Escape clears the readout and any locked series. The scrubbed column gets a soft band and fills in solid.
- Bars rise from the baseline one after another on entrance. With reduced motion they appear in place.
- Bars are capped at 36px wide and snapped to whole pixels, and the dither cell to whole device pixels, so edges and cells stay sharp.