figure
Figure plate with run-in caption, plus an editorial table with small-caps heads and hairline rules.
figurecaptiontablelayout
Preview
| Strategy | $/month | vs. baseline |
|---|---|---|
| No caching | $1,284 | 1.00× |
| 5-minute TTL | $212 | 6.06× |
| 1-hour TTL | $318 | 4.04× |
| Best | $212 | 6.06× |
Installation
shadcn CLI
Terminal
npx shadcn@latest add https://mocoui.site/r/figure.jsonManual
Copy each file from the Source section into its target path:
registry/figure/figure.tsx → components/moco/figure.tsx
registry/figure/figure.css → components/moco/figure.cssNeeds moco items: tokens
Usage
usage.tsx
import { FigurePlate, EdTable } from "./figure";
export default function FigureDemo() {
return (
<>
<FigurePlate
wide={false}
lead="Latency falls with cache depth."
caption="Each bar is the median of 1,000 runs against the same prefix."
>
<svg viewBox="0 0 240 80" width="240" height="80" role="img" aria-label="Three bars falling in height">
<rect x="20" y="10" width="40" height="60" fill="var(--moco-accent)" />
<rect x="100" y="30" width="40" height="40" fill="var(--moco-accent)" />
<rect x="180" y="50" width="40" height="20" fill="var(--moco-accent)" />
</svg>
</FigurePlate>
<EdTable
head={[{ label: "Strategy" }, { label: "$/month", numeric: true }, { label: "vs. baseline", numeric: true }]}
rows={[
["No caching", "$1,284", "1.00×"],
["5-minute TTL", "$212", "6.06×"],
["1-hour TTL", "$318", "4.04×"],
]}
summary={["Best", "$212", "6.06×"]}
bestIndex={1}
lead="The short TTL wins."
caption="Steady traffic keeps the cache warm, so the cheaper write premium dominates."
/>
</>
);
}
Source
components/moco/figure.tsx
import * as React from "react";
import "./figure.css";
export type FigurePlateProps = {
children: React.ReactNode;
/** Bold run-in phrase. Include its own full stop. */
lead: string;
caption: React.ReactNode;
id?: string;
/** Break out of a centred prose column to a wider measure. */
wide?: boolean;
};
/**
* A figure plate with a run-in caption. The caption sits outside the plate
* and always opens with a bold run-in phrase — consistency is the point.
*/
export function FigurePlate({ children, lead, caption, id, wide = true }: FigurePlateProps) {
return (
<figure id={id} className={`moco-plate-wrap${wide ? " moco-breakout" : ""}`}>
<div className="moco-plate">{children}</div>
<figcaption className="moco-cap">
<b className="moco-lead-in">{lead}</b> {caption}
</figcaption>
</figure>
);
}
export type EdTableProps = {
head: { label: string; numeric?: boolean }[];
rows: React.ReactNode[][];
/** Optional totals row, set off by a heavier rule. */
summary?: React.ReactNode[];
/** Bold run-in phrase for the caption. Include its own full stop. */
lead: string;
caption: React.ReactNode;
/** Index of the row to emphasise. */
bestIndex?: number;
};
/** Editorial table: caption below, small-caps heads, hairline rules, no zebra. */
export function EdTable({ head, rows, summary, lead, caption, bestIndex }: EdTableProps) {
return (
<table className="moco-ed">
<thead>
<tr>
{head.map((h) => (
<th key={h.label} className={h.numeric ? "moco-n" : undefined} scope="col">
{h.label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((r, i) => (
<tr key={i} data-best={i === bestIndex ? "true" : undefined}>
{r.map((c, j) => (
<td key={j} className={head[j]?.numeric ? "moco-n" : undefined}>
{c}
</td>
))}
</tr>
))}
{summary ? (
<tr className="moco-summary">
{summary.map((c, j) => (
<td key={j} className={head[j]?.numeric ? "moco-n" : undefined}>
{c}
</td>
))}
</tr>
) : null}
</tbody>
<caption>
<b className="moco-lead-in">{lead}</b> {caption}
</caption>
</table>
);
}
components/moco/figure.css
/* margin-block, not the shorthand — `.moco-breakout` owns margin-left. */
.moco-plate-wrap {
margin-block: 3rem;
}
/* Break out of a centred prose column. Override --moco-figure-wide to tune. */
.moco-breakout {
width: min(var(--moco-figure-wide, 1080px), calc(100vw - 3rem));
margin-left: 50%;
transform: translateX(-50%);
}
@media (max-width: 560px) {
.moco-breakout {
width: calc(100vw - 2.5rem);
}
}
.moco-plate {
position: relative;
background: var(--moco-panel);
border-radius: 2px;
padding: 2rem 1.75rem;
overflow-x: auto;
overflow-y: hidden;
background-image: radial-gradient(120% 90% at 50% 0%, rgba(0, 0, 0, 0.028), transparent 70%);
}
.moco-plate::-webkit-scrollbar {
height: 6px;
}
.moco-plate::-webkit-scrollbar-thumb {
background: var(--moco-line);
border-radius: 3px;
}
@media (max-width: 720px) {
.moco-plate {
padding: 1.25rem 1rem;
-webkit-mask-image: linear-gradient(to right, #000 93%, transparent 100%);
mask-image: linear-gradient(to right, #000 93%, transparent 100%);
}
}
figcaption.moco-cap {
display: block;
margin-top: 0.9rem;
font-size: 14.5px;
line-height: 1.6;
color: var(--moco-muted);
max-width: 680px;
font-family: var(--moco-serif);
font-variant-numeric: normal;
letter-spacing: 0;
}
/* The run-in phrase stays mono — it's a label, and the contrast against the
serif explanation is what makes captions scannable. */
figcaption.moco-cap .moco-lead-in,
table.moco-ed caption .moco-lead-in {
font-family: var(--moco-mono);
font-size: 0.88em;
color: var(--moco-ink);
font-weight: 500;
}
table.moco-ed {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
table.moco-ed caption {
caption-side: bottom;
text-align: left;
margin-top: 0.9rem;
font-size: 13.5px;
line-height: 1.6;
color: var(--moco-muted);
font-family: var(--moco-serif);
font-variant-numeric: normal;
letter-spacing: 0;
}
table.moco-ed th {
font-weight: 400;
font-size: 10.5px;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--moco-faint);
text-align: left;
padding: 0 0.75rem 0.6rem 0;
border-bottom: 1px solid var(--moco-line);
white-space: nowrap;
}
table.moco-ed td {
padding: 0.55rem 0.75rem 0.55rem 0;
border-bottom: 1px solid var(--moco-line);
color: var(--moco-body);
}
table.moco-ed th.moco-n,
table.moco-ed td.moco-n {
text-align: right;
font-variant-numeric: tabular-nums;
padding-right: 0;
white-space: nowrap;
}
table.moco-ed tr[data-best="true"] td {
color: var(--moco-ink);
font-weight: 700;
}
table.moco-ed tr.moco-summary td {
border-top: 1px solid var(--moco-muted);
border-bottom: none;
color: var(--moco-ink);
padding-top: 0.75rem;
}