Dashboard chart panel
An inline-SVG bar chart with the series also exposed as a real table.
Live preview
Source
This exact file renders the preview above.
import { Panel } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Divider } from '@/components/ui/layout'
/**
* Dashboard chart panel
*
* A bar chart drawn as inline SVG rather than pulling in a charting library.
* Foundry ships no chart dependency, so the series is also exposed as a real
* table for assistive tech and for anyone who wants the numbers — which is the
* correct treatment for a chart regardless of how it is drawn.
*/
const series = [
{ month: 'Oct', value: 128 },
{ month: 'Nov', value: 142 },
{ month: 'Dec', value: 131 },
{ month: 'Jan', value: 156 },
{ month: 'Feb', value: 168 },
{ month: 'Mar', value: 184 },
]
const max = Math.max(...series.map((point) => point.value))
export default function DashboardChartPanel() {
return (
<section className="bg-canvas p-4 sm:p-6">
<Panel
title="Monthly recurring revenue"
description="Thousands of USD, last six months"
headingLevel="h2"
action={<Badge tone="success">+12.4%</Badge>}
>
<div className="flex h-52 items-end gap-2 sm:gap-4" aria-hidden="true">
{series.map((point) => (
<div key={point.month} className="flex min-w-0 flex-1 flex-col items-center gap-2">
<span className="font-mono text-2xs text-ink-muted tabular-nums">{point.value}</span>
<div
className="w-full rounded-t-sm bg-accent transition-[height] duration-300"
style={{ height: `${(point.value / max) * 100}%` }}
/>
<span className="label-caps text-ink-subtle">{point.month}</span>
</div>
))}
</div>
<Divider className="my-5" weight="subtle" />
{/* The chart is decorative; this table is the accessible content. */}
<table className="w-full text-sm">
<caption className="sr-only">
Monthly recurring revenue in thousands of USD, October to March
</caption>
<thead>
<tr>
<th scope="col" className="label-caps pb-2 text-left text-ink-subtle">
Month
</th>
<th scope="col" className="label-caps pb-2 text-right text-ink-subtle">
Revenue
</th>
</tr>
</thead>
<tbody>
{series.map((point) => (
<tr key={point.month} className="border-t border-line-subtle">
<th scope="row" className="py-1.5 text-left font-normal text-ink-muted">
{point.month}
</th>
<td className="py-1.5 text-right font-mono text-ink tabular-nums">
${point.value},000
</td>
</tr>
))}
</tbody>
</table>
</Panel>
</section>
)
}
components/blocks/sections/dashboard/chart-panel.tsx
import { Panel } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Divider } from '@/components/ui/layout'
/**
* Dashboard chart panel
*
* A bar chart drawn as inline SVG rather than pulling in a charting library.
* Foundry ships no chart dependency, so the series is also exposed as a real
* table for assistive tech and for anyone who wants the numbers — which is the
* correct treatment for a chart regardless of how it is drawn.
*/
const series = [
{ month: 'Oct', value: 128 },
{ month: 'Nov', value: 142 },
{ month: 'Dec', value: 131 },
{ month: 'Jan', value: 156 },
{ month: 'Feb', value: 168 },
{ month: 'Mar', value: 184 },
]
const max = Math.max(...series.map((point) => point.value))
export default function DashboardChartPanel() {
return (
<section className="bg-canvas p-4 sm:p-6">
<Panel
title="Monthly recurring revenue"
description="Thousands of USD, last six months"
headingLevel="h2"
action={<Badge tone="success">+12.4%</Badge>}
>
<div className="flex h-52 items-end gap-2 sm:gap-4" aria-hidden="true">
{series.map((point) => (
<div key={point.month} className="flex min-w-0 flex-1 flex-col items-center gap-2">
<span className="font-mono text-2xs text-ink-muted tabular-nums">{point.value}</span>
<div
className="w-full rounded-t-sm bg-accent transition-[height] duration-300"
style={{ height: `${(point.value / max) * 100}%` }}
/>
<span className="label-caps text-ink-subtle">{point.month}</span>
</div>
))}
</div>
<Divider className="my-5" weight="subtle" />
{/* The chart is decorative; this table is the accessible content. */}
<table className="w-full text-sm">
<caption className="sr-only">
Monthly recurring revenue in thousands of USD, October to March
</caption>
<thead>
<tr>
<th scope="col" className="label-caps pb-2 text-left text-ink-subtle">
Month
</th>
<th scope="col" className="label-caps pb-2 text-right text-ink-subtle">
Revenue
</th>
</tr>
</thead>
<tbody>
{series.map((point) => (
<tr key={point.month} className="border-t border-line-subtle">
<th scope="row" className="py-1.5 text-left font-normal text-ink-muted">
{point.month}
</th>
<td className="py-1.5 text-right font-mono text-ink tabular-nums">
${point.value},000
</td>
</tr>
))}
</tbody>
</table>
</Panel>
</section>
)
}
components/ui/card.tsx
import type { HTMLAttributes, ReactNode, ElementType } from 'react'
import { cn } from '@/lib/cn'
/**
* Card & Panel
*
* Two containment primitives with deliberately different jobs:
*
* Card — a discrete, often interactive record in a collection.
* Panel — a titled region of a page, with an optional header action row.
*
* Keeping them separate is what stops the library from degenerating into
* "everything is a rounded box with a shadow".
*/
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
as?: ElementType
/** Forwarded when `as` renders a link. */
href?: string
/** `outline` is the default; `raised` adds elevation; `sunken` insets. */
tone?: 'outline' | 'raised' | 'sunken' | 'ghost' | 'accent'
/** Adds hover affordance. Only use when the whole card is a link/button. */
interactive?: boolean
padding?: 'none' | 'sm' | 'md' | 'lg'
}
const cardTones = {
outline: 'bg-surface border border-line',
raised: 'bg-surface-raised border border-line shadow-sm',
sunken: 'bg-surface-sunken border border-line-subtle',
ghost: 'bg-transparent border border-transparent',
accent: 'bg-accent-soft border border-accent-line',
} as const
const cardPadding = {
none: 'p-0',
sm: 'p-3',
md: 'p-card',
lg: 'p-6 sm:p-8',
} as const
export function Card({
as: Tag = 'div',
tone = 'outline',
interactive = false,
padding = 'md',
className,
children,
...props
}: CardProps) {
return (
<Tag
className={cn(
'rounded-lg',
cardTones[tone],
cardPadding[padding],
interactive &&
'transition-[border-color,box-shadow,background-color] duration-150 ease-standard hover:border-line-strong hover:shadow-sm',
className,
)}
{...props}
>
{children}
</Tag>
)
}
export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn('flex items-start justify-between gap-4', className)} {...props}>
{children}
</div>
)
}
export function CardTitle({
as: Tag = 'h3',
className,
children,
...props
}: HTMLAttributes<HTMLHeadingElement> & { as?: ElementType }) {
return (
<Tag className={cn('text-md leading-snug font-semibold text-ink-strong', className)} {...props}>
{children}
</Tag>
)
}
export function CardDescription({
className,
children,
...props
}: HTMLAttributes<HTMLParagraphElement>) {
return (
<p className={cn('text-sm leading-normal text-ink-muted', className)} {...props}>
{children}
</p>
)
}
export function CardFooter({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'mt-4 flex flex-wrap items-center gap-3 border-t border-line-subtle pt-4',
className,
)}
{...props}
>
{children}
</div>
)
}
export interface PanelProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
title: ReactNode
description?: ReactNode
/** Rendered on the right of the panel header. */
action?: ReactNode
/** Removes body padding — for tables and lists that manage their own. */
flush?: boolean
as?: ElementType
headingLevel?: 'h2' | 'h3' | 'h4'
}
export function Panel({
title,
description,
action,
flush = false,
as: Tag = 'section',
headingLevel: Heading = 'h3',
className,
children,
...props
}: PanelProps) {
return (
<Tag
className={cn('overflow-hidden rounded-lg border border-line bg-surface', className)}
{...props}
>
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-line-subtle bg-surface-sunken px-4 py-3">
<div className="min-w-0">
<Heading className="text-sm font-semibold text-ink-strong">{title}</Heading>
{description ? <p className="mt-0.5 text-xs text-ink-muted">{description}</p> : null}
</div>
{action ? <div className="flex shrink-0 items-center gap-2">{action}</div> : null}
</div>
<div className={cn(flush ? '' : 'p-card')}>{children}</div>
</Tag>
)
}
Demo source — adapt to your project. Foundry is not published as a package.
Usage
Foundry ships no chart dependency, so the chart is drawn inline and the series is exposed as a table — the correct treatment for a chart regardless of how it is drawn.
- Always provide the underlying numbers; a chart without them excludes people and prevents copying.
- Scale bars from the series maximum, not from a fixed ceiling.
Variants and states
Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.
- Six-month series
- Inline SVG bars
- Accessible data table
Accessibility
- Decorative chart
- The bar chart is aria-hidden; the table is the content.
- Row headers
- The data table uses scope on both column and row headers.
Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.
Related
All sectionsDashboard metric row
Four KPI tiles, two of them with inverted trend semantics.
starterFeatured3 variantsPanel
A titled region with a header action row and an optional flush body for tables and lists.
starter3 variantsAdmin starter
An internal console: overview, a filterable member table, orders, analytics and settings — all at compact density on the same components as the marketing starters.
advancedFeatured4 variants