Components · Data
Bar chart
Columns with their values on top and labels under them; the same data in a table for screen readers.
0.92% of the 2,062 sites in The UI Index have a chart (19 sites).
| Label | Value |
|---|---|
| Jun | 24 deploys |
| Jul | 31 deploys |
| Aug | 18 deploys |
| Sep | 42 deploys |
| Oct | 57 deploys |
| Nov | 63 deploys |
| Label | Value |
|---|---|
| Jun | 24 deploys |
| Jul | 31 deploys |
| Aug | 18 deploys |
| Sep | 42 deploys |
| Oct | 57 deploys |
| Nov | 63 deploys |
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ bar-chart.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ bar-chart.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.
import type { CSSProperties } from 'react';
import { cn } from '@/lib/utils';
import { barChart as c, heightOf, type Bar } from './bar-chart.variants';
export type { Bar };
export interface BarChartProps {
title: string;
data: Bar[];
/** Shown after numbers, e.g. " deploys" or "%". */
unit?: string;
/** A line at the top right, e.g. "Total 412". */
summary?: string;
className?: string;
}
/** Columns with their values on top, labels under them, and the same data in a table for screen readers. */
export function BarChart({ title, data, unit = '', summary, className }: BarChartProps) {
const max = Math.max(...data.map((d) => d.value));
return (
<figure className={cn(c.root, className)} style={{ '--bars': data.length } as CSSProperties}>
<figcaption className={c.head}>
<span className={c.title}>{title}</span>
{summary && <span className={c.total}>{summary}</span>}
</figcaption>
<div aria-hidden="true">
<div className={c.plot}>
{data.map((d) => (
<div key={d.label} className={c.col}>
<span className={c.bar} style={{ height: heightOf(d.value, max) }}><span className={c.value}>{d.value}</span></span>
</div>
))}
</div>
<div className={c.labels}>{data.map((d) => <span key={d.label} className={c.label}>{d.label}</span>)}</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>{d.value}{unit}</td></tr>)}</tbody>
</table>
</figure>
);
}/** A bar chart in HTML and CSS: columns grow from a baseline to their share of the largest value, each with its
* value on top and its label under it, so the numbers are read, not guessed. Ink when an accent is on (primary),
* otherwise black. The same data is in a table for screen readers; the drawing is hidden from them. The columns
* rise once on load; under reduced motion they stand still. */
export const barChart = {
root: 'w-full',
head: 'flex items-baseline justify-between gap-4',
title: 'text-sm font-medium',
total: 'text-sm text-muted-foreground tabular-nums',
// The value sits above its bar (absolute), so a bar's height is its share of the whole column.
plot: 'mt-6 grid h-48 items-end gap-2 border-b pt-6 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]',
col: 'group flex h-full flex-col items-center justify-end',
value: 'absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground',
bar: 'relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none',
labels: 'mt-2 grid gap-2 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]',
label: 'truncate text-center text-xs text-muted-foreground',
};
export type Bar = { label: string; value: number };
export const heightOf = (value: number, max: number) => `${Math.max(2, Math.round((value / (max || 1)) * 100))}%`;---
import { cn } from '@/lib/utils';
import { barChart as c, heightOf, type Bar } from './bar-chart.variants';
interface Props {
title: string;
data: Bar[];
/** Shown after numbers, e.g. " deploys" or "%". */
unit?: string;
/** A line at the top right, e.g. "Total 412". */
summary?: string;
class?: string;
}
const { title, data, unit = '', summary, class: className } = Astro.props;
const max = Math.max(...data.map((d) => d.value));
---
<figure class={cn(c.root, className)} style={`--bars: ${data.length}`}>
<figcaption class={c.head}>
<span class={c.title}>{title}</span>
{summary && <span class={c.total}>{summary}</span>}
</figcaption>
<div aria-hidden="true">
<div class={c.plot}>
{data.map((d) => (
<div class={c.col}>
<span class={c.bar} style={`height: ${heightOf(d.value, max)}`}><span class={c.value}>{d.value}</span></span>
</div>
))}
</div>
<div class={c.labels}>{data.map((d) => <span class={c.label}>{d.label}</span>)}</div>
</div>
<table class="sr-only">
<caption>{title}</caption>
<thead><tr><th scope="col">Label</th><th scope="col">Value</th></tr></thead>
<tbody>{data.map((d) => <tr><th scope="row">{d.label}</th><td>{d.value}{unit}</td></tr>)}</tbody>
</table>
</figure>/** A bar chart in HTML and CSS: columns grow from a baseline to their share of the largest value, each with its
* value on top and its label under it, so the numbers are read, not guessed. Ink when an accent is on (primary),
* otherwise black. The same data is in a table for screen readers; the drawing is hidden from them. The columns
* rise once on load; under reduced motion they stand still. */
export const barChart = {
root: 'w-full',
head: 'flex items-baseline justify-between gap-4',
title: 'text-sm font-medium',
total: 'text-sm text-muted-foreground tabular-nums',
// The value sits above its bar (absolute), so a bar's height is its share of the whole column.
plot: 'mt-6 grid h-48 items-end gap-2 border-b pt-6 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]',
col: 'group flex h-full flex-col items-center justify-end',
value: 'absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground',
bar: 'relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none',
labels: 'mt-2 grid gap-2 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]',
label: 'truncate text-center text-xs text-muted-foreground',
};
export type Bar = { label: string; value: number };
export const heightOf = (value: number, max: number) => `${Math.max(2, Math.round((value / (max || 1)) * 100))}%`;<figure class="w-full max-w-lg" style="--bars: 6">
<figcaption class="flex items-baseline justify-between gap-4">
<span class="text-sm font-medium">Deploys per month</span>
<span class="text-sm text-muted-foreground tabular-nums">235 in six months</span>
</figcaption>
<div aria-hidden="true">
<div class="mt-6 grid h-48 items-end gap-2 border-b pt-6 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]">
<div class="group flex h-full flex-col items-center justify-end">
<span class="relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none" style="height: 38%">
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground">24</span>
</span>
</div>
<div class="group flex h-full flex-col items-center justify-end">
<span class="relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none" style="height: 49%">
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground">31</span>
</span>
</div>
<div class="group flex h-full flex-col items-center justify-end">
<span class="relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none" style="height: 29%">
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground">18</span>
</span>
</div>
<div class="group flex h-full flex-col items-center justify-end">
<span class="relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none" style="height: 67%">
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground">42</span>
</span>
</div>
<div class="group flex h-full flex-col items-center justify-end">
<span class="relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none" style="height: 90%">
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground">57</span>
</span>
</div>
<div class="group flex h-full flex-col items-center justify-end">
<span class="relative w-full max-w-10 origin-bottom rounded-t-sm bg-primary transition-[opacity,scale] group-hover:opacity-85 animate-[caseroom-rise_600ms_ease-out_both] motion-reduce:animate-none" style="height: 100%">
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-muted-foreground tabular-nums transition-colors group-hover:text-foreground">63</span>
</span>
</div>
</div>
<div class="mt-2 grid gap-2 [grid-template-columns:repeat(var(--bars),minmax(0,1fr))]">
<span class="truncate text-center text-xs text-muted-foreground">Jun</span>
<span class="truncate text-center text-xs text-muted-foreground">Jul</span>
<span class="truncate text-center text-xs text-muted-foreground">Aug</span>
<span class="truncate text-center text-xs text-muted-foreground">Sep</span>
<span class="truncate text-center text-xs text-muted-foreground">Oct</span>
<span class="truncate text-center text-xs text-muted-foreground">Nov</span>
</div>
</div>
<table class="sr-only">
<caption>Deploys per month</caption>
<thead>
<tr>
<th scope="col">Label</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Jun</th>
<td>24 deploys</td>
</tr>
<tr>
<th scope="row">Jul</th>
<td>31 deploys</td>
</tr>
<tr>
<th scope="row">Aug</th>
<td>18 deploys</td>
</tr>
<tr>
<th scope="row">Sep</th>
<td>42 deploys</td>
</tr>
<tr>
<th scope="row">Oct</th>
<td>57 deploys</td>
</tr>
<tr>
<th scope="row">Nov</th>
<td>63 deploys</td>
</tr>
</tbody>
</table>
</figure>