zero-build-frontend
Zero-build frontend development with CDN-loaded React, Tailwind CSS, and vanilla JavaScript. Use when building static web apps without bundlers, creating Leaflet maps, integrating Google Sheets as database, or developing browser extensions. Covers patterns from rosen-frontend, NJCIC map, and PocketLink projects.
What this skill does
# Zero-build frontend development
Patterns for building production-quality web applications without build tools, bundlers, or complex toolchains.
## Picking a stack
Three current zero-build approaches, each with different trade-offs:
| Stack | When | Bundle size impact |
|---|---|---|
| **React via esm.sh + htm** | Component-heavy SPAs, existing React mental model, Tailwind styling | ~50 KB gzipped (React + ReactDOM + htm) |
| **htmx 2.x + server-rendered HTML** | CRUD apps, traditional MPA flow, want server-side state of truth | ~14 KB gzipped (htmx alone) |
| **Alpine.js 3.x + plain HTML** | Light interactivity sprinkled into mostly-static pages, no full SPA | ~15 KB gzipped (Alpine alone) |
You can mix htmx and Alpine.js in the same page — htmx handles server interactions, Alpine handles client-side UI state. Many production sites converge on this combo.
## ESM import maps
Import maps let you write `import x from 'react'` in a `<script type="module">` without a bundler — the browser resolves the bare specifier against the map. Stable in all major browsers since 2023.
```html
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/[email protected]",
"react-dom/client": "https://esm.sh/[email protected]/client",
"lodash-es": "https://esm.sh/[email protected]",
"@my-app/": "/src/"
},
"scopes": {
"https://esm.sh/": {
"scheduler": "https://esm.sh/[email protected]"
}
}
}
</script>
```
The `scopes` block lets a sub-tree of imports resolve differently. Useful when one CDN package needs a specific transitive dependency. The trailing `/` form (`"@my-app/": "/src/"`) lets you import any file under that prefix.
**Pin versions in production.** `esm.sh/react` (without a version) and `esm.sh/react@latest` resolve at request time and can shift under you. Use exact pinned versions or SHA-locked URLs.
## htmx 2.x — server-rendered interactivity
htmx 2.0 (released June 2024) lets you add AJAX, WebSockets, and SSE to plain HTML through `hx-*` attributes. The server sends HTML fragments; the client swaps them in. No JS framework required.
```html
<script src="https://unpkg.com/[email protected]"></script>
<!-- Click button → POST to server → swap response into #result -->
<button hx-post="/api/clicked" hx-target="#result" hx-swap="innerHTML">
Click me
</button>
<div id="result"></div>
<!-- Search-as-you-type with debounce -->
<input
type="search"
name="q"
hx-get="/api/search"
hx-trigger="input changed delay:300ms"
hx-target="#results"
/>
<div id="results"></div>
<!-- Infinite scroll -->
<div hx-get="/api/items?page=2"
hx-trigger="revealed"
hx-swap="afterend">
...
</div>
```
htmx 2.x dropped IE support and tightened the API; if you're on htmx 1.x and don't need to migrate, 1.x still receives security patches. New code should target 2.x.
## Alpine.js 3.x — client-side reactivity in HTML
Alpine.js (current 3.14+) is a minimal alternative to Vue/React for sprinkles of interactivity. State and behavior live as `x-*` attributes in the markup.
```html
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<!-- Toggle visibility -->
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>Content here</div>
</div>
<!-- Two-way binding + computed -->
<div x-data="{ first: '', last: '' }">
<input x-model="first" placeholder="First">
<input x-model="last" placeholder="Last">
<p x-text="`Hello, ${first} ${last}`"></p>
</div>
<!-- Fetch on mount -->
<div x-data="{ items: [] }"
x-init="items = await (await fetch('/api/items')).json()">
<template x-for="item in items" :key="item.id">
<li x-text="item.title"></li>
</template>
</div>
```
Alpine pairs naturally with htmx: htmx swaps a server-rendered fragment in, Alpine handles whatever client-side state that fragment needs (open/close, optimistic toggles, form validation).
## React via CDN (esm.sh)
### Basic setup
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zero-Build React App</title>
<!-- Tailwind CSS via CDN.
cdn.tailwindcss.com is the Play CDN; Tailwind explicitly recommends
it for prototyping only — it ships an in-browser JIT compiler that
runs at every page load. For production, use the standalone CLI
binary or the Vite/PostCSS plugin. Tailwind 4 (released Jan 2025)
is the current major; the Play CDN serves v3 by default. -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
display: ['Special Elite', 'monospace'],
body: ['Roboto Mono', 'monospace'],
},
colors: {
brand: {
primary: '#2dc8d2',
secondary: '#f34213',
dark: '#183642',
}
}
}
}
}
</script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Special+Elite&family=Roboto+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<!-- Custom styles -->
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="root"></div>
<!-- ES Module imports.
React 19 (Dec 2024) is the current major; 18.x still works fine
for sites that pin to it. Pin a specific version in production —
don't ship `react@latest`, since esm.sh resolves at request time. -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/[email protected]",
"react-dom/client": "https://esm.sh/[email protected]/client",
"htm": "https://esm.sh/[email protected]"
}
}
</script>
<script type="module" src="index.js"></script>
</body>
</html>
```
### React with htm (no JSX, no build)
```javascript
// index.js
import React, { useState, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import htm from 'htm';
// Bind htm to React.createElement
const html = htm.bind(React.createElement);
// Components use html`` instead of JSX
function App() {
const [records, setRecords] = useState([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
useEffect(() => {
loadData();
}, []);
async function loadData() {
try {
const response = await fetch('data/archive-data.json');
const data = await response.json();
setRecords(data.records);
} catch (error) {
console.error('Failed to load data:', error);
} finally {
setLoading(false);
}
}
const filtered = records.filter(r =>
r.title.toLowerCase().includes(search.toLowerCase())
);
if (loading) {
return html`<div class="flex items-center justify-center h-screen">
<div class="animate-spin w-8 h-8 border-4 border-brand-primary border-t-transparent rounded-full"></div>
</div>`;
}
return html`
<div class="min-h-screen bg-gray-900 text-white">
<header class="p-4 border-b border-gray-700">
<h1 class="font-display text-2xl">Archive Explorer</h1>
<input
type="text"
placeholder="Search records..."
value=${search}
onInput=${(e) => setSearch(e.target.value)}
class="mt-2 w-full p-2 bg-gray-800 rounded border border-gray-600 focus:border-brand-primary outline-none"
/>
</header>
<main class="p-4">
<${RecordList} records=${filtered} />
</main>
</div>
`;
}
function RecordList({ records }) {
return html`
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
${records.map(record => html`
<${RecordCard} key=${record.id} record=${record} />
`)}
</div>
`;
}
function RecordCard({ record }) {
return html`
<article class="p-4 bg-gray-800 rounded-lg border border-gray-700 hover:border-brand-primary transition-colors">
<h2 class="font-Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.