Names as Shared Foundations
When I write JSX in my editor and it eventually renders on your screen, a remarkable chain of abstractions is at work. My browser holds the low-level code that knows how to paint paragraphs and italic text, but the implementations differ between browsers and operating systems. What makes this portable is that we've agreed on names — <p> for a paragraph, <i> for italics. I can pass className to style them without knowing their internals, trusting the standards to produce consistent results.
These names don't have to point to browser internals. I can style my greeting with text-2xl and font-sans, which are defined by the Tailwind CSS library. The crucial insight is that names give us a way to build layers — screen driver developers focus on pixels, text rendering engineers handle glyphs, and I worry about whether my greeting looks right. Names let us forget what lies behind them.
Defining Custom Concepts
Building on the browser's vocabulary, I can create concepts the browser has never heard of. For example, what I think of as “a greeting for Alice” the browser sees as a paragraph with certain CSS classes and italic text. But from my perspective, it's a Greeting — my own concept. This naming gives me flexibility: I can reuse Greeting for multiple people, pass different data to it, and change all greetings in one place.
Yet there's a catch. The browser doesn't understand <Greeting>. To make this work, I need to translate my concept back to the browser's language. Defining the concept is the first step — alice only means something after I assign her data:
const alice = {
firstName: 'Alice',
birthYear: 1970
};A Greeting takes a person as input and produces a paragraph with “Hello, ” followed by that person's first name in italics, plus an exclamation mark. Unlike alice, which is pure data, Greeting is a function — it transforms data into UI. When I apply my definition, the greeting for Alice “unpacks” to browser-native JSX:
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>After substituting my concept with its definition, only the browser's own concepts remain.
Teaching a Computer to Translate
JSX, under the hood, constructs an object with type for the tag and props for the attributes. You can think of type as the “code” and props as the “data” — the result comes from plugging the data into the code. Here's a function that performs that translation:
function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
return type(props);
}When type is a function like Greeting, this function calls it with the given props, returning browser-ready JSX. What could you do with that output? You could serialize it to an HTML string for sending to the browser, or convert it to DOM update instructions — but for now, the critical point is that once translation completes, no custom concepts remain.
Handling Built-In Tags
Consider wrapping a greeting in a <details> tag so it appears collapsed:
<details>
<Greeting person={alice} />
</details>My translation approach hits a wall here. By convention, lowercase JSX tags like <details> refer to built-in browser elements, not functions I defined. In this case, type is the string 'details'. Trying to call a string as a function fails, because built-in tags have no accessible implementation — their behavior is opaque, living somewhere inside the browser itself.
The fix is to branch on the type of type. If it's a string representing a built-in tag, I shouldn't try to translate the element itself — but I must still process its children, since they may contain my own components:
function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
if (typeof type === 'function') {
return type(props);
} else if (typeof type === 'string') {
return {
type,
props: {
...props,
children: translateForBrowser(props.children)
}
};
}
}With this change, encountering <details>...</details> yields a new <details> element, but its contents get recursively translated so any embedded Greeting dissolves.
Nested Compositions
Now suppose I define an ExpandableGreeting that wraps a greeting in <details>. Running a nested composition through my translator reveals a bug: it processes the outer component's output but stops there. If that output contains another custom component, the result still isn't browser-ready.
The solution is recursive translation. After calling a function component, I take whatever JSX it returned and translate that as well. I also need stopping conditions for primitives like null or strings, and array handling for translation of each element:
function translateForBrowser(originalJSX) {
if (originalJSX == null || typeof originalJSX !== 'object') {
return originalJSX;
}
if (Array.isArray(originalJSX)) {
return originalJSX.map(translateForBrowser);
}
const { type, props } = originalJSX;
if (typeof type === 'function') {
const returnedJSX = type(props);
return translateForBrowser(returnedJSX);
} else if (typeof type === 'string') {
return {
type,
props: {
...props,
children: translateForBrowser(props.children)
}
};
}
}Now, translating <ExpandableGreeting person={alice} /> first dissolves the ExpandableGreeting:
<details>
<Greeting person={alice} />
</details>Then the inner Greeting dissolves too:
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>
</details>At that point the process stops — all names have been resolved to the browser's own vocabulary.
Tracing a Deeper Dissolution
With a WelcomePage component that renders multiple ExpandableGreetings, the sequence plays out at several levels:
function WelcomePage() {
return (
<section>
<h1 className="text-3xl font-sans pb-2">Welcome</h1>
<ExpandableGreeting person={alice} />
<ExpandableGreeting person={bob} />
<ExpandableGreeting person={crystal} />
</section>
);
}Starting with <WelcomePage />, WelcomePage first dissolves into its output of ExpandableGreetings:
<section>
<h1 className="text-3xl font-sans pb-2">Welcome</h1>
<ExpandableGreeting person={alice} />
<ExpandableGreeting person={bob} />
<ExpandableGreeting person={crystal} />
</section>Each ExpandableGreeting then dissolves into its <details> wrapper containing a Greeting:
<section>
<h1 className="text-3xl font-sans pb-2">Welcome</h1>
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>
</details>
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Bob</i>!
</p>
</details>
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Crystal</i>!
</p>
</details>
</section>Each Greeting dissolves into pure <p> and <i> markup, and the process terminates. This layered naming — where each abstraction builds on the previous one — is what lets me compose ideas freely while keeping the browser speaking its own language at the core.
One Thing Leads to Another
What starts with a mix of data and code transforms until no code remains, leaving only the final output behind. It’s a process that feels almost automatic — a cascade of small steps, each one triggering the next.
You could build this yourself, and you might even want to. But it would be far more convenient if there was a library that handled it for you.
Where Does the Work Happen?
Before you reach for that library, though, there’s a more basic question to settle. These transformations need to happen somewhere between your machine and the one that receives the result. So where does that actually take place?
The answer isn’t obvious. It could be on your end, where the data is first prepared and the process kicks off.



