Documentation menu

figure

Figure plate with run-in caption, plus an editorial table with small-caps heads and hairline rules.

figurecaptiontablelayout

Preview

Latency falls with cache depth. Each bar is the median of 1,000 runs against the same prefix.
Strategy$/monthvs. baseline
No caching$1,2841.00×
5-minute TTL$2126.06×
1-hour TTL$3184.04×
Best$2126.06×
The short TTL wins. Steady traffic keeps the cache warm, so the cheaper write premium dominates.

Installation

Ask Claude (MCP)

With the MCP server configured, just ask:

Claude
add the moco figure component

shadcn CLI

Terminal
npx shadcn@latest add https://mocoui.site/r/figure.json

Manual

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.css

Needs 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;
}