Templates
Article
One post, rendered from a real Markdown file through the site’s own pipeline.
The reading surface, against markup nobody in this repository authored: headings, tables, footnotes, LaTeX as MathML, task lists, fenced code, a flow diagram and a numbered pipeline, all out of one .md file.
One factor moves them all
What went wrong when the radius scale was four independent numbers
The radius scale used to be four numbers typed by hand, and a theme that wanted square corners typed four more. It looked fine at the setting somebody checked and was wrong at every other one. This is the arithmetic of why, and what replaced it.
The nesting law
Two rounded edges separated by a gap of are concentric only when the inner radius is the outer minus the gap:
Anything else pinches at the corner. The gap between the two curves is along the straight edges and narrows as it turns, and the eye reads that narrowing long before anyone can name it. A panel inset by 16px inside a frame rounded to therefore wants , and a frame wrapped 16px outside a panel rounded to wants .
Depth is a hairline and a change of ground, never a blur.
The White Reset, law 2
Why four numbers could not hold it
A theme that re-types every step can put the steps out of proportion with each
other — and once they are out of proportion, no single inset is right for two
different settings at the same time. Here is what the old round theme did:
| Step | Default | round |
Ratio |
|---|---|---|---|
--radius-sm |
6px | 10px | 1.67 |
--radius |
8px | 14px | 1.75 |
--radius-lg |
14px | 22px | 1.57 |
--radius-pill |
999px | 999px | 1.00 |
Three different ratios and an exemption. The pill was exempt on the grounds that a pill is a shape rather than a corner — which is true, and which is also how a "square corners" theme shipped with every button still a capsule.[1]
What replaced it
One factor, and the ladder derived from it:
:root,
[data-radius] {
--radius-factor: 1;
--radius-xs: calc(4px * var(--radius-factor));
--radius-sm: calc(6px * var(--radius-factor));
--radius: calc(8px * var(--radius-factor));
--radius-lg: calc(12px * var(--radius-factor));
--radius-pill: calc(999px * var(--radius-factor));
}A theme now sets one number. sharp sets it to 0 and everything squares off
together, the pill included; round sets it to 2 and every step doubles, so
the proportions hold.
The numbers themselves are not invented either, and that is deliberate: 8px for
a control and 12px for a panel is where Tailwind's scale, shadcn's --radius
± 2 and ± 4, Radix Themes' steps 3 and 4, and Material's xs/sm/md all
land within a pixel of each other. A design system is allowed one opinion per
decision, and this was not the decision worth spending it on.
Both directions of the nesting law are named, so a surface never has to guess:
--radius-rowsubtracts — a row inside a panel padded by 6px.--radius-frameadds — a frame sitting 16px outside a--radius-lgpanel.
Where the tokens are read
How a corner gets its number
- A theme sets the factor
- The ladder recomputes
- The nesting law derives two more
- A component reads a step
- The corner is drawn
The declaration sits on :root and on [data-radius], and that is
load-bearing rather than tidy. A custom property substitutes var() where it is
declared, so a ladder written only on the root bakes in the root's factor and
a themed subtree deeper in the page never reaches it — which is exactly the case
the themes page exercises, five radii on five wrappers of one document.
The cost of getting it wrong
Let be the number of nested surfaces on a screen and the number of radius settings a theme offers. Hand-typed steps make the number of arrangements somebody has to check ; a derived ladder makes it , because the proportions are a property of the ladder rather than of the setting. On this system that was against — and the twenty-four nobody checked are where every mismatched corner lived.
Press ⌘ K and type radius to see the tokens themselves.
A true circle is different again, and stays a circle: an avatar, a status dot, a spinner, a radio button. Those are geometry, not corners, and a radio that stopped being round would stop being distinguishable from a checkbox. ↩︎
Built from
Nothing here was styled specially for the template. That is the only arrangement where it stays honest as the system changes — a template with its own CSS stops being a test of the components and becomes a screenshot.
Source
'use client'
import { Article, Avatar, Badge, Button, Diagram, Separator, Steps, Tag } from '@misoto22/design'
import { ArrowLeft, Link2 } from 'lucide-react'
import { post } from '@/lib/posts'
const POST = post('one-factor-moves-them-all')
/**
* One post, rendered from real Markdown.
*
* This is the template that tests the READING surface, and it is the only one
* whose content does not come from a literal in this file: the body is a
* `.md` file under `src/content/posts`, put through the site's own build-time
* pipeline. That is deliberate, and it is what makes the test worth anything —
* a page of hand-written JSX proves the components look right and proves
* nothing about what happens when a pipeline hands the system a `<table>` it
* did not author.
*
* What the file exercises, and therefore what this proves the system styles:
* headings and their anchors, a standfirst, body copy on the measure, links,
* emphasis, inline and fenced code, a blockquote with an attribution, ordered,
* unordered and task lists, a GFM table wide enough to need its own scroll,
* a horizontal rule, footnotes with their back-references, keyboard keys,
* LaTeX both inline and displayed — rendered to MathML, so there is no second
* stylesheet and no font to load — and a flow diagram authored as a fenced
* ```diagram block and a numbered pipeline as a ```steps one.
*
* The blocks are prose and diagrams in the order they appeared: a diagram is a
* component and cannot arrive as an HTML string, so the pipeline lifts each one
* out and this maps over what is left. Everything else is one `Article`.
*/
export function Post() {
return (
<div className="flex flex-col">
<header className="flex items-center justify-between gap-4 border-b border-(--rule) px-6 py-4 @3xl:px-10">
<Button size="sm" variant="ghost" className="gap-2">
<ArrowLeft size={14} strokeWidth={1.5} aria-hidden />
Field Notes
</Button>
<Button size="sm" variant="ghost" iconOnly aria-label="Copy a link to this post">
<Link2 size={14} strokeWidth={1.5} aria-hidden />
</Button>
</header>
<div className="grid gap-10 px-6 py-10 @3xl:grid-cols-[minmax(0,1fr)_13rem] @3xl:px-10 @3xl:py-14">
<div className="flex min-w-0 flex-col gap-8">
<div className="flex max-w-(--w-reading) flex-col gap-4">
<div className="flex flex-wrap items-center gap-2">
<Badge tone="outline">{POST.meta.category}</Badge>
<span className="mono-meta text-(--ink-3-aa)">
{POST.meta.date} · {POST.readingMinutes} min
</span>
</div>
<h1 className="m-0 font-heading text-[length:var(--fs-lead)] font-normal leading-[1.12] tracking-[-0.02em] text-(--ink)">
{POST.meta.title}
</h1>
<p className="m-0 text-[length:var(--fs-item)] leading-[1.55] text-(--ink-2)">
{POST.meta.subtitle}
</p>
<div className="flex items-center gap-2.5 pt-1">
<Avatar fallback="HC" alt="" size="sm" />
<span className="text-[13px] text-(--ink)">{POST.meta.author}</span>
<span className="mono-meta text-(--ink-3-aa)">{POST.meta.role}</span>
</div>
</div>
<Separator />
{/* One Article per prose run, and the diagram between them as a real
component. The blocks are already in order — nothing here decides
where anything goes. */}
<div className="flex flex-col gap-6">
{POST.blocks.map((block, index) => {
if (block.kind === 'diagram') {
return <Diagram key={index} spec={block.spec} className="max-w-(--w-reading)" />
}
if (block.kind === 'steps') {
return <Steps key={index} {...block.spec} className="max-w-(--w-reading) py-2" />
}
return <Article key={index} as="div" html={block.html} />
})}
</div>
<Separator />
<div className="flex flex-wrap items-center gap-1.5">
{POST.meta.tags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</div>
</div>
{/* The contents rail, built from the headings the pipeline collected —
not from a second list somebody keeps in step by hand. */}
<nav aria-label="On this page" className="max-@3xl:hidden">
<div className="sticky top-6 flex flex-col gap-2 border-s border-(--rule) ps-4">
<span className="eyebrow text-(--ink-3-aa)">On this page</span>
{POST.toc.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className={`text-[13px] leading-snug text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:text-(--ink) ${
item.level === 3 ? 'ps-3' : ''
}`}
>
{item.text}
</a>
))}
</div>
</nav>
</div>
</div>
)
}