React UI
For any val that renders a UI, prefer to build it with React components in
files, unless the user states otherwise. The
templates/react-hono-starter
template is set up for this — start there with
instead of building from scratch.
File conventions
Put markup, styles, and scripts in real files — avoid template literal strings (e.g.
<html>...</html>`)`). Code in template strings has no syntax highlighting, no linting, no type checking, and is unreviewable.
- — React/JSX components, any UI with logic or interactivity
- — purely static markup
- — server code and scripts
Build UI
component by component in
files. Compose small components rather than rendering one giant page.
Styling: Twind + Tailwind
Prefer Twind to apply Tailwind utility classes at runtime — no build step required. Add the script to your HTML shell:
html
<script src="https://cdn.twind.style" crossorigin></script>
Then use Tailwind classes directly in JSX:
tsx
<div className="flex items-center gap-4 p-6 rounded-lg bg-white shadow">
<h1 className="text-2xl font-bold">Hello</h1>
</div>
Avoid inline
tags, CSS-in-JS objects, or separate
files, unless the user says otherwise.
Serving assets: versioned + immutable
Serve client modules with
from
— browsers cache
them immutably, and publishing bumps the val's version, which invalidates
automatically (full pattern: the
skill). A never-cached
shell (hono/jsx) stamps the entry and favicon with
:
tsx
// frontend/root.tsx — in the Root() HTML shell:
<script src={immutableFileUrl("/frontend/index.tsx")} type="module" />
// index.ts:
app.get("/", (c) => c.html(Root()));
app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));
View source link
Every UI val should expose a way for users to see and remix its source. Both parts are required:
- Backend route:
ts
import { parseVal } from "https://esm.town/v/std/utils/index.ts";
app.get("/source", (c) => c.redirect(parseVal().links.self.val));
- Visible link in the frontend:
tsx
<a href="/source">view source</a>
React version pinning
A common error —
"Cannot read properties of null (reading 'useState')"
— means a React sub-dependency is loading a different React version. Pin all React-related imports to 18.2.0:
ts
import SomeLib from "https://esm.sh/some-lib?deps=react@18.2.0,react-dom@18.2.0";
Assets
Do not use external images or hosted assets that may break. Prefer:
- Emojis or unicode symbols
- Inline SVG
- Icon fonts via CDN (Lucide, Font Awesome)
Surfacing client-side errors
To send browser errors back to val logs (visible via
), include this script in your HTML shell:
html
<script src="https://esm.town/v/std/catch"></script>
Verifying changes
After editing a UI val, call
to confirm the page renders without error, then check
for any client-side errors. Don't report the change as done without both.