When essays need to become tools
A small demo of the blog’s premise: mostly static prose, with React islands and live code only where the argument needs them.
Most essays should just be text. Fast to load, easy to quote, calm to read. But some ideas become clearer when the reader can touch the mechanism.
That is the point of this blog: keep the page quiet, then make specific parts executable when the argument needs more than prose.
Hydrate the smallest useful thing
This counter is a normal React component inside the post body. The article around it is static HTML. Astro hydrates this island only when it scrolls into view.
React island
Counter is interactive
The surrounding article is static HTML. This card hydrates independently when it becomes visible.
The source in this post is intentionally boring:
import Counter from '../../components/Counter.tsx';
<Counter client:visible initial={2} />
The trick is not the counter. The trick is keeping JavaScript scoped to the part of the essay that actually earns it.
Use demos when numbers matter
Sometimes a paragraph is weaker than a small instrument. This Monte Carlo demo lets the reader move the sample count and watch the estimate change in place.
Simulation
Estimate π in the article
Move the slider: React updates the sample count, recomputes the deterministic random points, and redraws the SVG without leaving the post.
- π estimate
- 3.13143
- Error
- 0.01016
A post about systems, probability, agents, interfaces, or product behavior can carry a tiny working model without turning the whole site into an app.
Make interaction feel optional
Motion is here for tactile explanations, not decoration. If an interaction does not clarify the idea, it should stay out of the way.
Motion
Drag the orb
Motion is included for micro-interactions, scrollytelling transitions, and gesture-heavy explainers.
Let readers inspect the claim
For code-heavy posts, an inspectable local code sketch lets the reader compare source with behavior without leaving the essay.
import { useState } from 'react';
export function SharpDefault() {
const [strict, setStrict] = useState(true);
return (
<section className="terminal-card">
<p className="eyebrow">live preview</p>
<h1>Small tools beat big theater.</h1>
<label>
<input
type="checkbox"
checked={strict}
onChange={(event) => setStrict(event.currentTarget.checked)}
/>
{strict ? 'Prefer sharp defaults' : 'Allow messy defaults'}
</label>
<pre>
{strict
? 'ship fewer knobs\nmake the important path obvious'
: 'add another setting\nhope users read the docs'}
</pre>
</section>
);
}
live preview
Small tools beat big theater.
Toggle the rule. The interface stays plain, but the behavior changes.
ship fewer knobs make the important path obvious
Pattern to copy for new posts
Create a post in src/content/blog/my-post.mdx:
---
title: 'My interactive post'
description: 'A one-line summary for cards and metadata.'
date: 2026-05-08
tags: [demo, react]
---
import MyWidget from '../../components/MyWidget.tsx';
Static prose here.
<MyWidget client:visible />
Useful hydration defaults:
client:visiblefor expensive demos that should hydrate as the reader reaches them.client:loadfor above-the-fold controls.client:idlefor non-urgent widgets.client:only="react"for browser-only components such as playgrounds.
The constraint is simple: plain essays by default, executable sections by exception.