JSX
Copy
// Modern-CSS dashboard widgets, no runtime JavaScript beyond rendering: one widget that re-lays
// itself out by its own width (container queries, @container + @min-[…]), stat cards whose rows
// line up through subgrid, and a conic-gradient donut built from the data.
// Usage: <ModernCssWidgets metric={{…}} stats={[…]} mix={{…}} /> — all props have demo data.
// Needs container queries and subgrid (Chrome 117, Safari 16, Firefox 110).
import { useId } from 'react';
const defaultMetric = {
title: 'Active trials', period: 'Week 42', value: '1,248', delta: '+9.2%', dir: 'up', tone: 'good', note: 'vs last week',
trend: [1100, 1113, 1105, 1132, 1147, 1139, 1162, 1171, 1155, 1183, 1203, 1213, 1183, 1224, 1248],
facts: [{ term: 'Converted', value: '214' }, { term: 'Expiring this week', value: '96' }, { term: 'Days to convert', value: '9.4' }],
};
const ICONS = {
users: <><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M19 8v6M22 11h-6" /></>,
dollar: <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />,
pulse: <path d="M22 12h-4l-3 9L9 3l-3 9H2" />,
undo: <><path d="M3 7v6h6" /><path d="M21 17a9 9 0 0 0-15-6.7L3 13" /></>,
};
const defaultStats = [
{ title: 'Signups', icon: 'users', value: '2,904', delta: '+11.3%', dir: 'up', tone: 'good', note: 'vs September' },
{ title: 'Average revenue per account', icon: 'dollar', value: '$37.03', delta: '+$1.20', dir: 'up', tone: 'good', note: 'vs September' },
{ title: 'Trial to paid', icon: 'pulse', value: '17.1%', delta: '−0.6 pts', dir: 'down', tone: 'bad', note: 'vs September' },
{ title: 'Refunds', icon: 'undo', value: '$1,284', delta: '−18%', dir: 'down', tone: 'good', note: 'lower is better' },
];
const defaultMix = {
title: 'Plan mix, paying accounts', date: 'October 16', unit: 'accounts',
slices: [
{ label: 'Starter', value: 1412, color: '#2a78d6' }, { label: 'Growth', value: 982, color: '#eb6834' },
{ label: 'Scale', value: 491, color: '#1baf7a' }, { label: 'Enterprise', value: 184, color: '#eda100' },
],
};
const n = new Intl.NumberFormat('en-US');
const card = 'min-w-0 rounded-[18px] border border-cq-line bg-white p-[18px] shadow-[0_1px_2px_rgba(30,27,46,.05),0_10px_28px_rgba(98,80,214,.08)]';
const label = 'mb-4 block text-[.72rem] tracking-[.12em] text-cq-muted uppercase';
function Delta({ dir, tone, children }) {
return (
<span className={`inline-flex items-center gap-[3px] rounded-full py-0.5 pr-2 pl-1.5 text-[.78rem] font-bold tabular-nums ${tone === 'good' ? 'bg-cq-good-bg text-cq-good' : 'bg-cq-bad-bg text-cq-bad'}`}>
<svg className="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d={dir === 'up' ? 'M12 19V5M5 12l7-7 7 7' : 'M12 5v14M19 12l-7 7-7-7'} />
</svg>
{children}
</span>
);
}
function Sparkline({ values, width = 120, height = 40, pad = 3 }) {
const min = Math.min(...values), max = Math.max(...values);
const line = 'M' + values.map((v, i) => `${+((i * width) / (values.length - 1)).toFixed(1)} ${+(height - pad - ((v - min) / (max - min || 1)) * (height - pad * 2)).toFixed(1)}`).join('L');
return (
<svg className="block h-12 w-full self-end overflow-visible [grid-area:spark] @min-[340px]:h-14 @min-[620px]:h-[72px]" viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none" aria-hidden="true">
<path className="fill-cq-accent opacity-12" d={`${line}L${width} ${height}L0 ${height}Z`} />
<path className="fill-none stroke-cq-accent stroke-2 [stroke-linecap:round] [stroke-linejoin:round]" vectorEffect="non-scaling-stroke" d={line} />
</svg>
);
}
// Lays itself out by the width of its @container wrapper: stacked, two columns, or a wide row.
function ResponsiveMetric({ m }) {
return (
<article className={`grid gap-x-[18px] gap-y-2.5 [grid-template-areas:'head'_'main'_'spark'_'stats'] @min-[340px]:grid-cols-2 @min-[340px]:[grid-template-areas:'head_head'_'main_spark'_'stats_stats'] @min-[620px]:grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)_200px] @min-[620px]:p-[22px] @min-[620px]:[grid-template-areas:'head_head_stats'_'main_spark_stats'] ${card}`}>
<div className="flex items-baseline justify-between gap-2 [grid-area:head]"><h3 className="text-[.84rem] font-semibold text-cq-muted">{m.title}</h3><span className="text-[.74rem] text-cq-muted">{m.period}</span></div>
<div className="flex flex-col gap-1.5 [grid-area:main]">
<p className="text-[clamp(1.7rem,9cqi,2.8rem)] leading-[1.15] font-[750] tracking-[-.02em] tabular-nums">{m.value}</p>
<p className="flex flex-wrap items-center gap-2 text-[.8rem] text-cq-muted"><Delta dir={m.dir} tone={m.tone}>{m.delta}</Delta>{m.note}</p>
</div>
<Sparkline values={m.trend} />
<dl className="grid gap-1.5 border-t border-cq-line pt-2.5 text-[.8rem] [grid-area:stats] @min-[340px]:grid-cols-3 @min-[620px]:grid-cols-1 @min-[620px]:content-center @min-[620px]:gap-3 @min-[620px]:border-t-0 @min-[620px]:border-l @min-[620px]:pt-0 @min-[620px]:pl-[18px] [&>div]:flex [&>div]:justify-between [&>div]:gap-2 @min-[340px]:[&>div]:flex-col @min-[340px]:[&>div]:gap-0 [&_dt]:text-cq-muted [&_dd]:font-bold [&_dd]:tabular-nums @min-[620px]:[&_dd]:text-[1.05rem]">
{m.facts.map((f) => <div key={f.term}><dt>{f.term}</dt><dd>{f.value}</dd></div>)}
</dl>
</article>
);
}
function SubgridStat({ s }) {
return (
<article className={`row-span-3 grid grid-rows-subgrid gap-2 ${card}`}>
<div className="flex items-start justify-between gap-2.5">
<h3 className="text-[.84rem] font-semibold text-cq-muted">{s.title}</h3>
<span className="grid size-[34px] flex-none place-items-center rounded-[10px] bg-[#efedfb] text-cq-accent" aria-hidden="true">
<svg className="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">{ICONS[s.icon]}</svg>
</span>
</div>
<p className="self-end text-[1.9rem] leading-[1.15] font-[750] tracking-[-.02em] tabular-nums">{s.value}</p>
<p className="flex flex-wrap items-center gap-2 text-[.8rem] text-cq-muted"><Delta dir={s.dir} tone={s.tone}>{s.delta}</Delta>{s.note}</p>
</article>
);
}
// conic-gradient stops with a 0.4% white gap after each slice.
function conicStops(slices, total) {
let at = 0;
const r1 = (v) => Math.round(v * 10) / 10;
return slices.map((s) => {
const start = at;
at += (s.value / total) * 100;
const end = r1(at);
return `${s.color} ${r1(start)}% ${r1(end - 0.4)}%,#fff ${r1(end - 0.4)}% ${end}%`;
}).join(',');
}
function ConicDonut({ mix }) {
const titleId = useId();
const total = mix.slices.reduce((a, s) => a + s.value, 0);
return (
<section className={`grid justify-items-center gap-[18px] @min-[460px]:grid-cols-[190px_minmax(0,1fr)] @min-[460px]:items-center @min-[460px]:gap-x-8 @min-[460px]:p-[22px] @min-[460px]:[grid-template-areas:'head_head'_'ring_legend'] ${card}`} aria-labelledby={titleId}>
<div className="justify-self-stretch @min-[460px]:[grid-area:head]">
<h3 className="text-[.84rem] font-semibold text-cq-muted" id={titleId}>{mix.title}</h3>
<p className="text-[.74rem] text-cq-muted">{mix.date} · {n.format(total)} {mix.unit}</p>
</div>
<div className="relative grid w-[min(100%,190px)] place-items-center @min-[460px]:[grid-area:ring]">
<div className="aspect-square w-full rounded-full [-webkit-mask:radial-gradient(farthest-side,transparent_61%,#000_calc(61%+1px))] [mask:radial-gradient(farthest-side,transparent_61%,#000_calc(61%+1px))]" style={{ background: `conic-gradient(${conicStops(mix.slices, total)})` }} aria-hidden="true" />
<p className="absolute inset-0 grid place-content-center text-center text-2xl leading-[1.1] font-[750] tabular-nums" aria-hidden="true">{n.format(total)}<small className="block text-[.72rem] font-medium text-cq-muted">{mix.unit}</small></p>
</div>
<ul className="grid gap-[9px] justify-self-stretch text-[.86rem] @min-[460px]:[grid-area:legend] @min-[460px]:max-w-[380px]">
{mix.slices.map((s) => (
<li key={s.label} className="grid grid-cols-[12px_1fr_auto_3.5ch] items-center gap-2.5 tabular-nums">
<i className="size-3 rounded-[3px]" style={{ background: s.color }} /><span>{s.label}</span><b className="font-bold">{n.format(s.value)}</b>
<span className="text-right text-cq-muted">{Math.round((s.value / total) * 100)}%</span>
</li>
))}
</ul>
</section>
);
}
export default function ModernCssWidgets({ metric = defaultMetric, stats = defaultStats, mix = defaultMix }) {
return (
<div className="flex flex-col gap-[46px]">
<div><span className={label}>One widget, three widths (container queries)</span>
<div className="flex flex-col gap-[18px]">
{['max-w-[250px]', 'max-w-[440px]', ''].map((w) => (
<div key={w || 'full'} className={`@container ${w}`}><ResponsiveMetric m={metric} /></div>
))}
</div>
</div>
<div><span className={label}>Stat cards on subgrid rows</span>
<div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,210px),1fr))] gap-4">{stats.map((s) => <SubgridStat key={s.title} s={s} />)}</div>
</div>
<div><span className={label}>Conic-gradient donut, narrow and wide</span>
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,300px),1fr))] gap-4">
<div className="@container"><ConicDonut mix={mix} /></div>
<div className="@container min-[701px]:col-span-2"><ConicDonut mix={mix} /></div>
</div>
</div>
</div>
);
}