- readsCoinGecko market caps and the day's moves
- becomescap → mass (area + ink); the move → polarity
- writes
--w--cat - 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.
area = market cap — the heavier the asset, the more page it takes · color = the 24-hour move — direction is hue, magnitude is intensity
- Aa tile area = the weighting signal (cap or volume)
- color = the move — direction is hue, magnitude is intensity
- sparkline = the week's price path (tile-local scale, big tiles only)
- glow = the engine's live density (
--d) — hold a tile and the field gathers
- 01 Bitcoin btc$61,517.00 $1.2T cap▼ 1.70% ▼ 7.76%
- 02 Ethereum eth$1,632.80 $197.5B cap▼ 1.94% ▼ 12.12%
- 03 Tether usdt$0.999374 $186.8B cap▼ 0.02% ▲ 0.07%
- 04 BNB bnb$591.20 $79.7B cap▼ 0.36% ▼ 9.10%
- 05 USDC usdc$0.999812 $75.1B cap▲ 0.02% ▲ 0.01%
- 06 XRP xrp$1.13 $70.3B cap▼ 2.11% ▼ 5.81%
- 07 Solana sol$64.76 $37.5B cap▼ 1.23% ▼ 12.45%
- 08 TRON trx$0.322211 $30.6B cap▼ 1.14% ▼ 3.03%
- 09 Figure Heloc figr_heloc$1.03 $19.2B cap▲ 0.54% ▼ 0.25%
- 10 Dogecoin doge$0.084628 $13.1B cap▼ 0.77% ▼ 8.36%
- 11 Hyperliquid hype$57.48 $12.8B cap▼ 8.56% ▼ 17.26%
- 12 USDS usds$0.999692 $10.6B cap▼ 0.02% ▼ 0.00%
- 13 LEO Token leo$9.48 $8.7B cap▲ 0.88% ▼ 5.55%
- 14 Rain rain$0.012646 $7.9B cap▼ 3.61% ▼ 9.49%
- 15 Zcash zec$427.80 $7.2B cap▼ 4.50% ▼ 27.59%
- 16 Stellar xlm$0.19139 $6.5B cap▼ 4.34% ▼ 13.43%
- 17 Canton cc$0.164211 $6.4B cap▲ 3.24% ▲ 9.56%
- 18 Cardano ada$0.165396 $6.2B cap▼ 0.76% ▼ 22.07%
- 19 WhiteBIT Coin wbt$51.00 $6B cap▲ 14.43% ▲ 5.11%
- 20 Monero xmr$310.62 $5.8B cap▼ 0.30% ▼ 4.52%
- 21 Chainlink link$7.82 $5.7B cap▼ 0.26% ▼ 6.33%
- 22 Toncoin ton$1.69 $4.5B cap▲ 0.58% ▼ 13.95%
- 23 USD1 usd1$0.998826 $4.5B cap▲ 0.07% ▲ 0.07%
- 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
<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 capdata-c24/data-c7— the raw moves the lens reads--cat— the polarity color the CSS mixes indata-hot— the engine's live density (--d) gathers toward 1 while helddata-field-visual-for— binds the sparkline to its record as a measurement
2 — mass is area; polarity is 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. // once a minute (skipped while the tab
// is hidden) — the same CoinGecko
// endpoint the snapshot script uses:
const data = await (
await fetch(GECKO_URL)).json();
for (const c of data) {
const tile = byId.get(c.id);
if (!tile) continue; // unknown ids:
// not bodies on this page
tile.dataset.cap = String(c.market_cap);
price.textContent =
fmtPrice(c.current_price);
flash(price); // ▲ teal / ▼ red tick
spark.setAttribute("d",
sparkPath(c.sparkline_in_7d.price));
}
applyLens(); // --cat through the
// ACTIVE lens
recomputeWeights(); // --w — the scoped
// field re-measures
// a poll NEVER re-sorts or re-tiers —
// hostile under the cursor. Tiers
// re-settle on the next lens 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.