Client-side JavaScript
SkillWeb & browsingUse when a val needs to ship JavaScript that runs in the browser, React apps, vanilla DOM scripts, canvas/games, htmx/Alpine, or any client-side module beyond a single inline snippet. Explains how Val Town serves transpiled .ts/.tsx/.jsx modules with no build step, how the browser resolves their imports, and how to load third-party deps.
Use Client-side JavaScript in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Client-side JavaScript and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Client-side JavaScript skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by hashgraph-online/awesome-codex-plugins in plugins/val-town/plugins/skills/client-side-js/SKILL.md and read by Ahel’s review.
Val Town has no build step and no bundler. A client-side module is just a file
in your val that you serve over HTTP; Val Town transpiles it per request. You point
a <script type="module"> at a route that returns the file, and the browser runs
it. There is nothing to configure (no webpack/vite/esbuild).
Serving a module
serveFile from std/utils reads a file and serves it with the correct
Content-Type. For .ts, .tsx, and .jsx it transpiles to JavaScript —
strips types, compiles JSX — and serves text/javascript. You serve the source
file; the browser receives runnable JS.
import { serveFile } from "https://esm.town/v/std/utils/index.ts";
// in any HTTP handler — serve a client module at some URL path
app.get("/app.tsx", (c) => serveFile("/app.tsx"));
Then load it from your HTML:
<script type="module" src="/app.tsx"></script>
The path you serve at and the file's location are up to you. A common shortcut is a wildcard that serves a whole directory of modules and assets:
app.get("/client/**/*", (c) => serveFile(c.req.path));
serveFile defaults to the current val. If you call it from a non-entrypoint file
and paths don't resolve, pass import.meta.url as the second argument.
Default: versioned, immutably cached modules
serveImmutableFile makes your val's frontend faster by letting browsers cache
files immutably; publishing bumps the val's version, which invalidates
automatically. Measured: repeat visits 665ms → 157ms with zero asset requests.
import { immutableFileUrl, serveImmutableFile } from "https://esm.town/v/std/utils/index.ts";
app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));
In the never-cached HTML shell, stamp the entry module:
immutableFileUrl("/frontend/index.tsx") → /__immutable/42/frontend/index.tsx
(42 = the val's current version). Relative imports resolve under the same prefix,
so only the entry needs stamping — one route and one stamped URL cover the whole
client graph.
- Old-version URLs 404 after a publish (like Next.js build assets); a reload picks up the new version.
- Retrofitting an existing val without touching its shell? Also point its old
file route at
serveImmutableFile— bare paths then 302 into versioned space, at one redirect per page view.
Alternative: serve directly from esm.town
Every val file already has a public esm.town URL that transpiles on demand, so you
can skip serveFile and point a script straight at it:
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>
serveFile is usually preferred because the module is served same-origin from a
path you control, and you don't have to hardcode your own val URL.
How imports resolve in the browser
The transpiler does not bundle or rewrite imports — it only strips types and JSX. So every import in a client module must be something the browser can fetch as a URL:
-
Local imports need explicit extensions.
import { x } from "./util.ts"resolves to/util.ts(or relative to the served path) and must be served too — by the same route or a wildcard. Omitting the extension (./util) 404s. -
Third-party deps need full ESM URLs. Bare specifiers like
import React from "react"don't resolve in the browser. Import from a CDN such as esm.sh, with versions pinned:import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";An import map in the HTML is an option if you want bare specifiers in client code.
The same model works for any client code — React, vanilla DOM scripts, a canvas
game loop, Alpine, htmx. Only the imports differ; for a plain .ts module with no
dependencies there's nothing to load from a CDN at all.
React specifics
Pin all React-family imports to the same version (18.2.0) and pass
?deps=react@18.2.0,react-dom@18.2.0 on libraries that depend on React. Mismatched
copies cause Cannot read properties of null (reading 'useState'). See the
react-ui skill for JSX and styling conventions.
What not to do
- No app logic in inline
<script>blobs or template-string HTML. Put client code in real.ts/.tsxfiles so it's typed, linted, and reviewable. A few lines of inline bootstrap are fine; the app is not. - No bundler / build command. There is no build step to add.
serveStaticfrom Hono does not work on Val Town — useserveFile.
Verifying changes
Fetch the module's URL (e.g. /app.tsx) and confirm it returns text/javascript,
not HTML or an error. Add https://esm.town/v/std/catch to the HTML shell to pipe
browser errors into get_logs, then load the page and check the logs. Don't report
the change as done without both.
Signals
- GitHub stars
- 1k
- Forks
- 316
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
client-side-js- Source
- github.com/hashgraph-online/awesome-codex-plugins
github.com/hashgraph-online/awesome-codex-plugins
Related picks
Skill · thedaviddias
The pick for JavaScriptmodern-javascript-patterns
Skill · wshobson
The pick for JavaScriptsetup-ts-deep-modules
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScriptclerk-nextjs-patterns
Skill · clerk
The pick for Next.jslogs-nextjs
Skill · posthog
The pick for Next.js