• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
kamalnrf

kamalnrf

val-tool-playground-skill

Skill for building consistent tool vals with Pico CSS
Public
Like
val-tool-playground-skill
Home
Code
4
references
1
README.md
SKILL.md
H
main.ts
Connections
Environment variables
Branches
1
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
SKILL.md
Code
/
SKILL.md
Search
…
Viewing readonly version of main branch: v27
View latest version
SKILL.md
name:
val-tool-playground
description:
Build developer tools and playgrounds on Val Town — single-purpose utilities that serve both a human UI and a programmatic API from one endpoint. Use this skill whenever creating tool vals, utility vals, playground vals, or any Val Town HTTP val that does one thing well with a web UI and composable API. Trigger when the user says "create a tool", "build a playground", "make a utility", or wants a quick web tool for inspecting, decoding, parsing, looking up, generating, or exploring something.

Val Tool Playground

A skill for building developer tools and playgrounds on Val Town. These are single-purpose utilities designed around one core principle: every tool is both a UI and an API.

Think: JWT decoders, DNS lookup tools, HTTP header inspectors, cron expression testers, hash/encode utilities, QR generators, web content extractors, OG tag previewers, sitemap validators, user-agent parsers, regex playgrounds, color pickers, API response explorers, CSS animation playgrounds, diff viewers.

Philosophy

Every tool is a URL

The most useful developer tools are the ones you can compose. A tool that only works through a web form is a dead end. A tool that returns structured output from a URL is a building block.

Design every tool so that:

  • A human can open it in a browser and use it interactively
  • A script can curl it and pipe the output somewhere
  • An LLM can fetch it as context for a prompt
  • Another tool can call it as a dependency

This last point is increasingly important. Tools should be agent-friendly — their output should be parseable and useful when an LLM fetches the URL directly. Think of every tool as a potential node in an agentic workflow.

URL as interface

When the tool processes a URL as input, make it a path parameter:

https://your-tool.val.run/https://example.com/article

This is more composable than query params because you can construct it by concatenation. Query params are for modifiers and flags (?json=1, ?selector=.content), not primary input.

For tools with non-URL input (text, tokens, expressions), use query params or POST with a JSON body — whatever maps most naturally to how the tool would be called programmatically.

Raw output by default

The API endpoint should return the most useful raw format — plain text, markdown, CSV — not JSON. JSON is for metadata.

Why: A tool that returns markdown can be piped directly into an LLM context window. A tool that returns JSON requires parsing first. Optimize for the common case.

Provide ?json=1 for when callers need structured metadata alongside the content.

One concern per tool

Each val does one thing. Don't build a Swiss Army knife. Build a screwdriver that works perfectly and can be composed with other screwdrivers.

Composition happens at the URL level: chain tools together by passing one tool's output URL to another. This only works if each tool has a clear, singular purpose.

Architecture

Dual-mode HTTP handler

Every tool val serves from a single HTTP endpoint with routing based on the request:

GET /                        → HTML page (the playground UI)
GET /path-or-params          → raw output (the API)
GET /path-or-params?json=1   → structured JSON with metadata
POST / with JSON body        → process input and return JSON

The UI is a convenience layer. The API is the product.

Handler structure

export default async function (req: Request): Promise<Response> { const url = new URL(req.url); // 1. Handle OPTIONS for CORS preflight // 2. Handle POST (for paste/upload input) // 3. Check for target input in path or query params // → Return raw output or JSON based on ?json param // 4. Fall through to HTML UI }

File structure

Simple tools (single concern, short HTML):

main.ts          # HTTP handler + inline HTML renderer
README.md

Complex tools (multiple features, rich UI):

main.ts          # HTTP handler (routing only)
lib/
  renderer.ts    # HTML page generation
  parser.ts      # Core logic
  types.ts       # TypeScript interfaces
README.md

Keep main.ts thin — just routing. Push logic into lib/.

API Design

CORS

Always add CORS headers on API responses so tools are callable from anywhere:

const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", };

Error handling

Return proper HTTP status codes with error messages in both modes:

// API consumers get JSON errors if (wantJson) return Response.json({ error: e.message }, { status: 502, headers: cors }); // Raw consumers get plain text errors return new Response(`Error: ${e.message}`, { status: 502, headers: cors });

In the UI, show errors inline — never use alert().

UI Principles

We use Pico CSS v2 as the styling foundation. It provides semantic HTML styling with zero classes, system theme support, and consistent form/button/card components.

Core rules:

  • System theme only — never force light or dark
  • No external images or favicons — use emoji or inline SVG
  • Vanilla HTML/CSS/JS — no frameworks for tools
  • Prefer Pico's native components over custom CSS (accordions, tooltips, buttons, switches, dropdowns are all built in)
  • Use ARIA attributes for state (aria-busy for loading, aria-invalid for validation, aria-selected for tabs)
  • No Hono — raw Request/Response is sufficient for tool vals

For the base HTML template, component patterns, tabs implementation, and the full list of native Pico components, see references/pico-ui-patterns.md.

Visual anatomy

Every tool follows this layout skeleton:

┌─────────────────────────────────┐
│ 📄 Tool Name                    │  ← emoji h1
│ One-line description + links    │  ← subtitle (hgroup or p)
├─────────────────────────────────┤
│ [Tab A] [Tab B]                 │  ← optional: when tool has multiple modes
├─────────────────────────────────┤
│ [input...............] [Action] │  ← fieldset role="group"
│ ▸ Advanced options              │  ← optional: <details> for extra params
├─────────────────────────────────┤
│ Title: X · Words: N · 42ms     │  ← optional: metadata bar (.meta)
│ ┌───────────────────────[Copy] │
│ │ output content                │  ← pre.result with copy button overlay
│ │ ...                           │
│ └───────────────────────────── │
├─────────────────────────────────┤
│ API docs / proxy hint           │  ← optional: inline API reference
├─────────────────────────────────┤
│ author · source                 │  ← footer with links
└─────────────────────────────────┘

Always present:

  • <main class="container"> at 800–900px max-width
  • Emoji <h1> + descriptive subtitle
  • Primary input via <fieldset role="group"> (input + action button)
  • Results container (dynamically rendered)
  • Footer with author link

Added based on complexity:

  • Tabs — when the tool has multiple input modes (e.g., decode vs encode, URL vs paste HTML)
  • <details> for advanced options — content selectors, extra flags, format choices
  • Metadata bar — flex row of extracted stats (title, word count, timing, etc.)
  • Copy button — absolute-positioned over output <pre> block
  • API hint section — inline docs showing the proxy URL pattern and params

Val Town Conventions

Imports

Pin npm versions. Use npm: prefix. Fall back to https://esm.sh/ if sandboxed:

import { Window } from "npm:happy-dom@17.4.4";

Handler signature

export default async function (req: Request): Promise<Response> { ... }

Constraints

  • No filesystem (fs, Deno.readFile) — use SQLite/blob for persistence
  • No subprocess (Deno.Command, child_process)
  • 30s execution timeout
  • Don't use Response.redirect — broken on Val Town

README

Keep brief. Lead with the API — the UI is self-explanatory:

# Tool Name One-line description. **Live:** https://tool.val.run ## API \`\`\` https://tool.val.run/https://example.com \`\`\` ### Options | Param | Description | |-------|-------------| | `?json=1` | Full JSON with metadata | ## Limitations - Known constraints

Naming

  • Val name: kebab-case (jwt-decoder, dns-lookup)
  • Description: terse, starts with verb or noun
  • Emoji in H1 only, not in val name or description

Anti-Patterns

  • Don't forget the API — every tool must work without the UI
  • Don't over-abstract — single main.ts is fine for simple tools
  • Don't force a color theme — respect system preference
  • Don't reach for frameworks — if you need React, it's probably not a tool val
  • Don't build multi-purpose tools — split concerns into separate vals that compose
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
AboutAlternativesPricingBlogNewsletterCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.