1. readsa Hacker News discussion tree
  2. becomessubtree size → mass; reply tempo → hue
  3. writes--w--cat
  4. you seebig subthreads loom in the type; fast exchanges run warm, slow ones cool

No canvas is drawn here — the field is invisible; these variables are its only output. Without it you would build: comment scores plus a collapse heuristic.

field-ui · invisible fields · threads

Arguments have structure.

A comment thread is a binding structure: every reply binds to the comment it answers, and busy subtrees heat up while dead ends go quiet. Most thread UIs flatten that into uniform rows, so you reconstruct the argument by reading all of it. These are the first 160 comments of a real Hacker News discussion — the CrowdStrike boot-loop thread — run as a field.

Each comment is a body whose mass is the subtree it spawned — a comment that started an argument is heavy; a reply nobody answered sits at the floor. Reply tempo shows as heat: warm comments landed minutes after their parent, cool ones took hours. Hover or focus any comment and its full ancestor chain plus its direct replies light up — the discussion's real shape, readable before you read a word. Collapse any subtree with its caret — the hidden comments leave the field, not just the page — and expand it back when you want the argument. The field is invisible: no particle swarm, only type weight, ink, and anchor.

Field
Heat lens

size = subtree — the replies a comment spawned · color = tempo — warm replies landed within minutes of their parent, cool ones took hours

How it's built

Every comment is ordinary semantic HTML — an <li> carrying its id, its parent's id, and its tempo as data attributes. Mass is the log-normalized count of replies it spawned; CSS turns that one number into type heft, ink, and anchor. The binding chain is a parent-id walk — no SVG, no canvas, no per-element listeners beyond hover and focus. Two live channels sit on top, engine-written every frame: --d (local density, gathered by data-hot on hover — the hovered comment's density also charges its chain's connector ink) and --field-attention (the recipe's eased attention). They touch only ink; the tree's mass is fixed history. Collapse is the same walk run the other way: a caret hides a comment's whole subtree (every descendant whose parent chain crosses it), and the scoped field is destroyed and re-applied over the bodies still visible — collapsed comments stop participating instead of lingering as zero-size ghosts.

1 — mark each comment as a field body

HTML
<!-- each comment is a body. data-feedback: the engine writes
     --d (live local density) back every frame. data-hot:
     hover/focus a comment and the field gathers toward it.
     data-top marks a top-level comment (the glow channel). -->
<li
  data-body="attract"
  data-strength="1.36"
  data-feedback
  data-hot
  data-top
  data-id="41002198" data-parent="41002195"
  data-tempo="0.91"
  style="--w: 0.60; --cat: hsl(31 74% 64%); --depth: 0;"
>
  <!-- your ordinary HTML here -->
</li>
  • data-body="attract" — registers as a field participant
  • data-strength — mass, from the subtree the comment spawned
  • data-parent — the binding: which comment this one answers
  • data-feedback — opt in to --field-* writebacks (--d)
  • data-hot — hover/focus gathers the field toward this comment

2 — CSS reads weight, depth + the live channels

CSS
.th-c {
  /* --depth = min(depth, 6) — the indent; --d is the
     ENGINE's lane: live density, data-hot gathers it */
  --live: var(--d, 0);
  padding-left: calc(var(--depth) * var(--ind));
}
.th-row {
  opacity: calc(0.58 + var(--w) * 0.42);
  box-shadow: 0 0 calc(var(--live) * 14px) -6px
    color-mix(in srgb,
      var(--cat) calc(var(--live) * 50%), transparent);
  background: color-mix(in srgb, var(--text)
    calc(1.5% + var(--w) * 4% + var(--live) * 4%
      + var(--field-attention, 0) * 4%),
    transparent);
}
/* top-level comments with big subtrees burn steadier */
.th-c[data-top] .th-row {
  box-shadow: 0 0
    calc(var(--w) * 8px + var(--live) * 14px) -6px
    color-mix(in srgb, var(--cat)
      calc(var(--w) * 30% + var(--live) * 50%),
      transparent);
}

One log-normalized subtree count drives weight, ink, and anchor; the indentation (--depth) supplies the tree geometry for free. --d belongs to the engine — live local density, written back every frame.

3 — the chain and the collapse are the same walk

// hover or focus a comment:
row.classList.add("lit");

// walk the ancestor ids up to the story
let p = byId.get(row.dataset.parent);
while (p) {
  p.classList.add("cited");
  p = byId.get(p.dataset.parent);
}

// and light the direct replies
for (const kid of kids.get(row.dataset.id))
  kid.classList.add("cited");

// the chain CHARGES: while a comment is lit, mirror its
// live --d (engine-written) onto the list as --chain —
// the connector ink reads it
list.style.setProperty("--chain",
  row.style.getPropertyValue("--d") || "0");

// the scoped field runs invisible (renderless) and asks
// for the attention metric lane (--field-attention) —
// applyRecipe options, not a hand-spread recipe:
//   { renderless: true, extraMetrics: ["attention"] }