1. readsCoinGecko market caps and the day's moves
  2. becomescap → mass (area + ink); the move → polarity
  3. writes--w--cat
  4. you seebig caps take more page; direction is hue, magnitude is intensity

No canvas is drawn here — the field is invisible; these variables are its only output. Without it you would build: a treemap library plus a color scale you maintain.

field-ui · invisible fields · market

The tape, felt.

A market table treats a $1.2T asset and a $4B asset as the same row height, the same ink. The only signal left is the sort order. This is the same snapshot from CoinGecko run as a field: market cap becomes gravitational mass, and mass is area — heavier assets take more of the page, with bigger tiles, darker ink, heavier type.

The day's move polarizes each body: direction is color (teal-green up, red-pink down), magnitude is intensity — a −0.1% drift barely registers, a −17% slide burns. Switch the window to 7d and the same tiles repolarize; reweight by volume and the mosaic re-tiers and re-settles around where the trading actually happened. Toggle the field off and it collapses back to an even grid — every asset the same size, the same ink, the live glow gone dark.

Field
Weight by
Move window
Data snapshot · June 10, 2026

area = market cap — the heavier the asset, the more page it takes · color = the 24-hour move — direction is hue, magnitude is intensity

  1. 01
    Bitcoin btc
    $61,517.00 $1.2T cap
    ▼ 1.70% ▼ 7.76%
  2. 02
    Ethereum eth
    $1,632.80 $197.5B cap
    ▼ 1.94% ▼ 12.12%
  3. 03
    Tether usdt
    $0.999374 $186.8B cap
    ▼ 0.02% ▲ 0.07%
  4. 04
    BNB bnb
    $591.20 $79.7B cap
    ▼ 0.36% ▼ 9.10%
  5. 05
    USDC usdc
    $0.999812 $75.1B cap
    ▲ 0.02% ▲ 0.01%
  6. 06
    XRP xrp
    $1.13 $70.3B cap
    ▼ 2.11% ▼ 5.81%
  7. 07
    Solana sol
    $64.76 $37.5B cap
    ▼ 1.23% ▼ 12.45%
  8. 08
    TRON trx
    $0.322211 $30.6B cap
    ▼ 1.14% ▼ 3.03%
  9. 09
    Figure Heloc figr_heloc
    $1.03 $19.2B cap
    ▲ 0.54% ▼ 0.25%
  10. 10
    Dogecoin doge
    $0.084628 $13.1B cap
    ▼ 0.77% ▼ 8.36%
  11. 11
    Hyperliquid hype
    $57.48 $12.8B cap
    ▼ 8.56% ▼ 17.26%
  12. 12
    USDS usds
    $0.999692 $10.6B cap
    ▼ 0.02% ▼ 0.00%
  13. 13
    LEO Token leo
    $9.48 $8.7B cap
    ▲ 0.88% ▼ 5.55%
  14. 14
    Rain rain
    $0.012646 $7.9B cap
    ▼ 3.61% ▼ 9.49%
  15. 15
    Zcash zec
    $427.80 $7.2B cap
    ▼ 4.50% ▼ 27.59%
  16. 16
    Stellar xlm
    $0.19139 $6.5B cap
    ▼ 4.34% ▼ 13.43%
  17. 17
    Canton cc
    $0.164211 $6.4B cap
    ▲ 3.24% ▲ 9.56%
  18. 18
    Cardano ada
    $0.165396 $6.2B cap
    ▼ 0.76% ▼ 22.07%
  19. 19
    WhiteBIT Coin wbt
    $51.00 $6B cap
    ▲ 14.43% ▲ 5.11%
  20. 20
    Monero xmr
    $310.62 $5.8B cap
    ▼ 0.30% ▼ 4.52%
  21. 22
    Toncoin ton
    $1.69 $4.5B cap
    ▲ 0.58% ▼ 13.95%
  22. 23
    USD1 usd1
    $0.998826 $4.5B cap
    ▲ 0.07% ▲ 0.07%
  23. 24
    Ethena USDe usde
    $0.999148 $4.5B cap
    ▼ 0.02% ▲ 0.03%

How it's built

Every asset is an ordinary <li> with a few data attributes. field-ui reads them, runs an invisible field over the mosaic, and the tiles carry the measurement — cap as area and ink, the move as color. No canvas, no chart library beyond one inline <path> per tile. Four channels do the work: --w is the server-computed weight, --cat the lens color, --d the engine's live local density (hold a tile and the field gathers), and --field-attention the recipe's eased attention metric. Each sparkline also declares its provenance with data-field-visual-for, so the binding between chart and record is inspectable.

1 — each asset is a field body

HTML
<li id="mk-bitcoin"
  data-body="attract"
  data-strength="1.87"
  data-feedback
  data-hot
  data-cap="1235425696510"
  data-price="61517"
  data-c24="-1.70" data-c7="-7.76"
  style="--w: 0.92; --cat: hsl(350 70% 64%);"
>
  <!-- name · price · badge -->
  <svg aria-hidden="true"
    data-field-visual-for="#mk-bitcoin"
    data-field-visual-role="measurement">
    <path d="" pathLength="100" />
  </svg>
</li>
  • data-strength — gravitational mass from log-normalized cap
  • data-c24 / data-c7 — the raw moves the lens reads
  • --cat — the polarity color the CSS mixes in
  • data-hot — the engine's live density (--d) gathers toward 1 while held
  • data-field-visual-for — binds the sparkline to its record as a measurement

2 — mass is area; polarity is CSS

CSS
/* tier classes set the tile's footprint */
.mk-t1 { grid-column: span 3; grid-row: span 2; }
.mk-t2 { grid-column: span 2; grid-row: span 2; }

.mk-tile {
  /* --w = log-normalized cap (0..1).
     --d = the engine's LIVE local density —
     data-hot gathers it toward 1 on hold. */
  --live: var(--d, 0);
  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);
}
/* the sparkline breathes with the tile */
.mk-spark path {
  stroke-width: calc(1.5 + var(--live) * 0.9);
  filter: drop-shadow(0 0
    calc(var(--live) * 6px) var(--cat));
}
/* entry draw-in — a keyframe, not a dash
   steady state: paths carry
   pathLength="100", the BASE rule sets no
   stroke-dasharray, and the dash lives
   only inside the animation — a drawn
   line computes stroke-dasharray: none */
@keyframes mk-spark-draw {
  from { stroke-dasharray: 100;
         stroke-dashoffset: 100;
         opacity: 1; }
  to   { stroke-dasharray: 100;
         stroke-dashoffset: 0; }
}

The runtime sets --cat from the chosen window — hue is direction, intensity is magnitude — and one color-mix() tints the tile against the hairline. --d is live: hold a tile and the halo and sparkline thicken as the field gathers, then ease back.

3 — re-tier on lens change; live updates in place

// recompute --w from cap or volume,
// swap the tier class, re-sort, then
// FLIP tiles whose size did not change:
const first = new Map(tiles.map(
  (t) => [t, t.getBoundingClientRect()]));
ordered.forEach((t) => list.appendChild(t));
ordered.forEach((t) => {
  const a = first.get(t);
  const b = t.getBoundingClientRect();
  t.style.transform = `translate(
    ${a.left - b.left}px,
    ${a.top - b.top}px)`;
  requestAnimationFrame(() =>
    (t.style.transform = ""));
});
// re-tiered tiles settle with a fade
// instead — translate cannot honestly
// animate a size change.

When the CoinGecko API is reachable, the runtime re-polls it once a minute and updates the snapshot's 24 tiles in place — prices, badges, colors, weights, sparklines. A poll never re-sorts or re-tiers the mosaic (that would be hostile under the cursor); tiers re-settle the next time you change the lens. Ids the snapshot does not know are ignored.