# Simon HTML Tools

> Build single-file HTML tools following Simon Willison's patterns - self-contained HTML+JS+CSS applications optimized for LLM generation, no build step, CDN dependencies. Use when creating browser-based tools, utilities, or demos that should be (1) Self-contained in one HTML file, (2) Easy to distribute and host statically, (3) Quick to prototype with LLMs, (4) Client-side only with no server requirements. Ideal for data visualization, API explorers, format converters, debugging tools, and interactive demos.

- Skill: `majiayu000/simon-html-tools` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/simon-html-tools`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/simon-html-tools/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/simon-html-tools

---


# Simon's HTML Tools

Build production-quality single-file HTML tools following patterns from [Simon Willison's collection](https://tools.simonwillison.net/).

## Core Philosophy

HTML tools are self-contained browser applications that:
- Combine HTML, CSS, and JavaScript in a single file
- Load dependencies from CDNs (no build step)
- Can be copy-pasted from LLM responses
- Host anywhere as static files
- Remain small and maintainable (typically <500 lines)

## Essential Principles

### 1. Single File, No Build Step

**Always avoid React and build tools:**
```
Prompt: "Build a JSON to YAML converter. No React."
```

Why no React:
- JSX requires compilation
- Slower to render in LLM environments
- More prone to bugs
- Harder to copy/paste and distribute

### 2. Load Dependencies from CDNs

Use version-pinned URLs from cdnjs.com or jsdelivr.com:

```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
```

### 3. Keep It Small

Target <500 lines total:
- Easy for LLMs to understand and modify
- Can be rewritten from scratch in minutes
- Maintainability becomes less critical

## Implementation Patterns

### State Persistence in URL

Store application state in the URL for bookmarking and sharing:

```javascript
// Save state to hash
function saveState(state) {
  window.location.hash = encodeURIComponent(JSON.stringify(state));
}

// Load state from hash
function loadState() {
  if (!window.location.hash) return null;
  try {
    return JSON.parse(decodeURIComponent(window.location.hash.slice(1)));
  } catch (e) {
    return null;
  }
}
```

### Secrets in localStorage

Never put API keys in URLs or code. Use localStorage:

```javascript
function getApiKey(serviceName) {
  const key = localStorage.getItem(`${serviceName}_api_key`);
  if (!key) {
    const newKey = prompt(`Enter your ${serviceName} API key:`);
    if (newKey) localStorage.setItem(`${serviceName}_api_key`, newKey);
    return newKey;
  }
  return key;
}
```

### Advanced Clipboard Operations

```javascript
document.addEventListener('paste', async (e) => {
  e.preventDefault();
  
  for (const item of e.clipboardData.items) {
    if (item.type.startsWith('image/')) {
      const file = item.getAsFile();
      // Handle image
    } else if (item.type === 'text/html') {
      const html = await new Promise(r => item.getAsString(r));
      // Handle HTML
    }
  }
});

// Copy to clipboard with fallback
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
  } catch (err) {
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    document.body.removeChild(textarea);
  }
}
```

### Working with Local Files

```javascript
document.getElementById('fileInput').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  
  // Read as text
  const text = await file.text();
  
  // Or as Data URL
  const reader = new FileReader();
  reader.onload = (e) => processImage(e.target.result);
  reader.readAsDataURL(file);
});
```

### Generate Downloadable Files

```javascript
function downloadText(content, filename, mimeType = 'text/plain') {
  const blob = new Blob([content], { type: mimeType });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}
```

### CORS-Enabled APIs

```javascript
// PyPI
async function fetchPackage(name) {
  const res = await fetch(`https://pypi.org/pypi/${name}/json`);
  return await res.json();
}

// GitHub raw files
async function fetchGitHubFile(owner, repo, path) {
  const res = await fetch(
    `https://raw.githubusercontent.com/${owner}/${repo}/main/${path}`
  );
  return await res.text();
}
```

### LLM API Direct Access

```javascript
async function callClaude(prompt) {
  const apiKey = getApiKey('claude');
  const res = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01'
    },
    body: JSON.stringify({
      model: 'claude-3-haiku-20240307',
      max_tokens: 1024,
      messages: [{ role: 'user', content: prompt }]
    })
  });
  return await res.json();
}
```

### Pyodide for Python

```javascript
let pyodide = null;

async function initPyodide() {
  if (!pyodide) {
    pyodide = await loadPyodide();
    await pyodide.loadPackage(['numpy', 'pandas']);
  }
  return pyodide;
}

async function runPython(code) {
  const py = await initPyodide();
  return await py.runPythonAsync(code);
}
```

## Template Structure

See references/template.md for complete HTML template with best practices.

## Useful Libraries from CDN

**Data Processing:**
- js-yaml, papaparse, jszip, dompurify

**Visualization:**
- Chart.js, Plotly, Leaflet, highlight.js

**File Processing:**
- PDF.js, Tesseract.js, exif-js, mammoth

**Utilities:**
- date-fns, fuse.js, marked, diff

## Hosting

**Recommended: GitHub Pages**

1. Create repository
2. Settings → Pages → Deploy from branch
3. Copy HTML file
4. Access at `https://username.github.io/repo/tool.html`

**Avoid LLM platforms** (restrictive sandboxes, warnings, less reliable)

## Development Workflow

### 1. Prototype in Claude/ChatGPT

```
Build an artifact that converts CSV to JSON with preview. 
Use PapaParse from CDN. Add download button. No React.
```

### 2. Upgrade to Coding Agent

For complex projects: Claude Code or Codex CLI

### 3. Remix Existing Tools

```
Look at the pypi-changelog tool. Build a similar tool 
for npm packages showing version diffs.
```

## Checklist

- [ ] Single HTML file (no build step)
- [ ] "No React" in prompts
- [ ] CDN dependencies with versions
- [ ] Code under 500 lines
- [ ] Error handling for APIs
- [ ] API keys in localStorage
- [ ] State in URL or localStorage
- [ ] Mobile responsive
- [ ] Footer with source link

## Resources

- **references/template.md** - Complete HTML template structure
- **references/patterns.md** - Detailed implementation patterns
- **references/libraries.md** - Comprehensive CDN library catalog
- **references/examples.md** - Real-world tool examples

