# Javascript

> Vanilla JavaScript rules - ES2024 stable / ES2025 target, defensive programming, ESM modules, DOM, events, fetch, browser APIs and data structures

- Skill: `14bryanespinoza/javascript` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 14bryanespinoza/javascript`
- Raw SKILL.md: https://api.skillmd.com/api/skills/14bryanespinoza/javascript/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: 14BryanEspinoza (https://skillmd.com/u/14bryanespinoza)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/14bryanespinoza/javascript

---


# Vanilla JavaScript - Rules and Conventions

---

## 1. Philosophy

1. **Vanilla first** — No framework until the project proves it needs
   one. The platform covers interactivity, state, storage and animation;
   libraries enter only against documented gaps.
2. **Baseline-first** — ES2024 stable, targeting ES2025. Baseline
   features are safe; "Newly available" requires a runtime-support
   check; anything newer goes through the Methodology gate (section 21).
3. **Platform over dependencies** — Each release absorbs former library
   territory (`structuredClone`, `groupBy`, Set operations, Intl). When
   the platform covers it, drop the dependency.
4. **Defensive by default** — Validate inputs, guard null results, abort
   stale requests, remove listeners.
5. **XSS-safe DOM writes** — `textContent` is the default; HTML-parsing
   APIs accept trusted markup only ([Security](../security/SKILL.md)).

## 2. Minimum Versions

| Technology | Minimum Version                                    |
| ---------- | -------------------------------------------------- |
| JavaScript | ES2024 stable (target: ES2025); notes per feature  |
| Node.js    | 22+ (22.6+ for most ES2025; 23+ for `Promise.try`) |

Baseline-first policy: prefer APIs labeled Baseline on MDN. Features
marked "Newly available" must be checked against the project's
browserslist targets before use.

## 3. When to Use JavaScript

Use JavaScript for behavior the platform cannot declare:

| Case                   | Justification                       |
| ---------------------- | ----------------------------------- |
| Dynamic interactivity  | State-driven UI beyond CSS toggles  |
| DOM manipulation       | Create/update content dynamically   |
| Fetch/AJAX             | Get/send data without reloading     |
| Client-side validation | Real-time feedback and submission   |
| Browser APIs           | Storage, clipboard, observers, etc. |

Do NOT use JavaScript where declarative alternatives exist — they are
more robust, accessible and work without JS:

| Instead of...     | Use...                     |
| ----------------- | -------------------------- |
| Simple animations | CSS Transitions/Animations |
| Simple tooltips   | CSS-only or `title`        |
| Simple modals     | HTML `<dialog>`            |
| Simple accordions | `<details>/<summary>`      |
| Simple carousels  | CSS scroll snap            |
| Simple dropdowns  | HTML `<select>`            |

## 4. Preferred APIs

| Instead of...                  | Use...                                             |
| ------------------------------ | -------------------------------------------------- |
| `var`                          | `const` (default), `let` (if reassigned)           |
| `function` expressions         | Arrow functions when appropriate                   |
| Callbacks                      | `async/await` + `Promise`                          |
| String concatenation           | Template literals `` `hello ${name}` ``            |
| `$.ajax`                       | `fetch`                                            |
| `for` loops                    | `map`/`filter`/`reduce` (`for...of` when awaiting) |
| `JSON.parse(JSON.stringify())` | `structuredClone()`                                |
| `setTimeout`-driven animation  | CSS transitions                                    |
| JS animation when unavoidable  | Web Animations API (section 17)                    |
| Scroll/viewport detection      | IntersectionObserver (section 16)                  |

## 5. Modern Data Structures

**Rule:** plain objects stay the default for simple string-keyed records;
use `Map` for keys of any type, `Set` for uniqueness and `WeakMap` for
object-keyed metadata that must not leak.

```javascript
const users = new Map();               // keys of any type
users.set(42, { name: "Anna" });
users.get(42);                         // { name: "Anna" }
for (const [key, value] of users) {}   // insertion order

const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]

// WeakMap — object keys; entries vanish with them (no GC leak)
const metadata = new WeakMap();
metadata.set(document.querySelector(".card"), { renderedAt: Date.now() });
```

Set operations (ES2025 · Baseline 2024) replace hand-rolled set algebra
and utility-library helpers:

```javascript
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
a.union(b);               // Set(6) {1, 2, 3, 4, 5, 6}
a.intersection(b);        // Set(2) {3, 4}
a.difference(b);          // Set(2) {1, 2}
a.symmetricDifference(b); // Set(4) {1, 2, 5, 6}
a.isSubsetOf(b);          // false
a.isSupersetOf(b);        // false
a.isDisjointFrom(b);      // false
```

## 6. Language APIs (ES2022 → ES2025)

**Promise.withResolvers() (ES2024)** externalizes `resolve`/`reject`
when bridging callback-based code. **Promise.try() (ES2025 · Baseline
2025)** runs a sync-or-async function and always returns a Promise —
synchronous throws become rejections with no manual try/catch. Ideal for
middleware and plugin hooks (Node 23+, see Minimum Versions):

```javascript
const { promise, resolve, reject } = Promise.withResolvers();
externalCallback((err, data) => (err ? reject(err) : resolve(data)));

const result = Promise.try(task); // sync throw → rejection
```

**Object.groupBy() / Map.groupBy() (ES2024)** — plain object for string
keys, Map otherwise:

```javascript
const products = [
  { name: "Laptop", category: "tech", price: 1200 },
  { name: "Shirt", category: "clothing", price: 30 },
];
Object.groupBy(products, (p) => p.category);
Map.groupBy(products, (p) =>
  p.price < 50 ? "budget" : p.price < 500 ? "mid" : "premium");
```

**structuredClone() (Baseline 2022)** deep-clones Date, Map, Set,
RegExp, ArrayBuffer — but not functions, Symbols or DOM nodes:

```javascript
const cloned = structuredClone({ date: new Date(), tags: ["js"] });
cloned.tags.push("ts"); // original untouched
```

**Immutable array methods (ES2023 · Baseline 2023)** return copies and
never mutate the source — prefer them over `sort()`/`reverse()`/`splice()`
in shared state:

```javascript
const arr = [3, 1, 2];
arr.toSorted();      // [1, 2, 3]
arr.toReversed();    // [2, 1, 3]
arr.toSpliced(0, 1); // [1, 2]
arr.with(0, 9);      // [9, 1, 2] — arr untouched throughout
```

**Object.hasOwn() (ES2022)** is the safe own-property check — works on
`Object.create(null)` objects where `hasOwnProperty` throws.
**findLast()/findLastIndex() (ES2023)** search from the end:

```javascript
const obj = Object.create(null);
obj.name = "test";
Object.hasOwn(obj, "name");                        // true
[1, 2, 3, 4, 5].findLast((n) => n % 2 === 0);      // 4
[1, 2, 3, 4, 5].findLastIndex((n) => n % 2 === 0); // 3
```

**Error.cause (ES2022)** preserves the original error when rethrowing
with context:

```javascript
try {
  await loadDashboard();
} catch (error) {
  throw new Error("Dashboard failed", { cause: error });
}
```

## 7. Advanced Patterns

Reach for these only when the simpler tool genuinely does not fit.

**Proxy + Reflect** intercepts writes centrally instead of sprinkling
validation at call sites. **Intl.Segmenter (Baseline 2023)** gives
locale-aware word/sentence/grapheme boundaries — `split("")` breaks
emoji and accents:

```javascript
const validator = {
  set(target, key, value) {
    if (key === "age" && (typeof value !== "number" || value < 0)) {
      throw new TypeError("age must be a positive number");
    }
    return Reflect.set(target, key, value);
  },
};
new Proxy({}, validator).age = -5; // TypeError

const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" });
[...graphemes.segment("👍🏽")].length; // 1 grapheme, not two code points
```

**RegExp.escape (ES2025 · Baseline 2025)** escapes regex syntax before
embedding untrusted strings in dynamic patterns — prevents broken
regexes and regex injection. Newly available: verify runtime support:

```javascript
const userInput = "price is $5.00 (USD)";
new RegExp(userInput); // ❌ $ . ( ) are special characters
new RegExp(RegExp.escape(userInput)).test(userInput + " today"); // ✅ true
```

**Iterator helpers (ES2025 · Baseline 2025)** give lazy
`map`/`filter`/`take`/`drop`/`zip` directly on iterators — no
intermediate arrays, works on infinite generators:

```javascript
function* naturals() {
  let i = 1;
  while (true) yield i++;
}
naturals().take(5).toArray();          // [1, 2, 3, 4, 5]
naturals().drop(10).take(3).toArray(); // [11, 12, 13]
[...naturals().filter((n) => n % 2 === 0).take(3)]; // [2, 4, 6]
Iterator.zip(["Anna", "Louis"], [25, 30]).toArray();
// [["Anna", 25], ["Louis", 30]]
```

## 8. ES Modules

Standard layout: `src/js/main.js` entry point plus feature modules
(`editor.js`) and shared helpers (`utils.js`). Module scripts execute
deferred — no `DOMContentLoaded` wrapper needed. `import.meta.url`
exposes the current module URL when required:

```javascript
// editor.js
export function initEditor() { /* ... */ }
export const EDITOR_CONFIG = { maxLength: 10000 };

// main.js
import { initEditor } from "./editor.js";
initEditor();
```

**Dynamic import()** loads on demand for code splitting — the bundler
emits a separate chunk:

```javascript
button.addEventListener("click", async () => {
  const { showToast } = await import("./toast.js");
  showToast("Saved");
});
```

**Import attributes (ES2025)** — `with { type }` makes the runtime
validate the type before evaluating (MIME-confusion safeguard); JSON
modules expose only `default`:

```javascript
import config from "./config.json" with { type: "json" };
const { default: pkg } = await import("./pkg.json", {
  with: { type: "json" },
});
```

**Import maps** resolve bare specifiers in the browser without bundler:

```html
<script type="importmap">
  { "imports": { "date-fns": "/vendor/date-fns.esm.js" } }
</script>
```

## 9. DOM Selection

```javascript
const element = document.querySelector(".class");     // first match
const elements = document.querySelectorAll(".class"); // all matches
const el = document.getElementById("my-id");          // fastest by id
element.dataset.property = "value";                   // data-property
```

> `querySelector` returns `null` on no match — always guard before use
> (see Defensive Programming, section 13).

## 10. DOM Manipulation

**Rule:** build content with `createElement` + `textContent`. HTML-
parsing APIs (`innerHTML`, `insertAdjacentHTML`) are reserved for
static, developer-owned markup — never user data
([Security](../security/SKILL.md)):

```javascript
const div = document.createElement("div");
div.className = "card";
div.textContent = userInput; // ✅ rendered as text, XSS-safe

container.innerHTML = '<div class="ad">Static markup</div>'; // trusted only
container.insertAdjacentHTML("beforeend", tpl);
// positions: beforebegin | afterbegin | beforeend | afterend
```

Insertion family — `append` accepts strings and nodes;
`replaceChildren` clears and fills in one operation:

```javascript
parent.append("Text", element);
parent.prepend(element);
sibling.before(element);
sibling.after(element);
oldElement.replaceWith(newElement);
parent.replaceChildren(a, b);
element.remove();
```

Clone inert `<template>` content (see [HTML](../html/SKILL.md)); prefer
direct ARIA IDL properties over `setAttribute`; drive Popover with its
methods instead of manual show/hide state:

```javascript
const card = document.querySelector("#tpl-card").content.cloneNode(true);
card.querySelector(".title").textContent = title;
container.append(card);

button.ariaExpanded = "true";
popover.showPopover(); // also hidePopover() / togglePopover()
```

## 11. Events

**Listener options and cleanup** — `{ once, passive }` for one-shot and
scroll-path handlers; an `AbortController` signal removes a whole group
of listeners in one call:

```javascript
element.addEventListener("scroll", onScroll, { passive: true });
dialog.addEventListener("close", onClose, { once: true });

const controller = new AbortController();
window.addEventListener("resize", onResize, { signal: controller.signal });
controller.abort(); // removes every listener bound to this signal
```

**No inline handlers** — `onclick` couples markup to globals and fights
CSP ([Security](../security/SKILL.md)). Bind via `data-action`; delegate
on a stable parent with `closest()` for dynamic children:

```javascript
// Markup: <button type="button" data-action="save">Save</button>
document.querySelectorAll("[data-action]").forEach((el) => {
  el.addEventListener("click", handleAction);
});

list.addEventListener("click", (event) => {
  const item = event.target.closest(".list__item");
  if (!item) return; // click landed outside an item
});
```

**Pointer events** unify mouse, touch and pen; pair custom gestures with
CSS `touch-action: none` when scrolling must be prevented:

```javascript
element.addEventListener("pointerdown", (e) => {
  console.log(e.pointerType, e.clientX, e.clientY); // mouse | touch | pen
});
```

**Keyboard events** — only non-native interactive elements
(`div[role="button"][tabindex="0"]`) need manual Enter/Space handling.
Real `<button>`/`<a>` already fire `click`: adding `keydown` fires the
action twice:

```javascript
custom.addEventListener("keydown", (event) => {
  if (event.key !== "Enter" && event.key !== " ") return;
  event.preventDefault();
  activate(custom);
});
```

Focus management and the full keyboard policy live in the
[Accessibility skill](../accessibility/SKILL.md).

## 12. Fetch

**Always check response.ok** — `fetch` rejects on network failure only;
HTTP error statuses resolve normally and must be thrown explicitly:

```javascript
async function getJSON(url, options) {
  const response = await fetch(url, options);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

const data = await getJSON("/api/items"); // throws on 4xx/5xx/network

// POST JSON — same contract
await getJSON("/api/items", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(item),
});
```

Auth headers and cookies belong to [Auth](../auth/SKILL.md); status-code
contracts to [API Design](../api-design/SKILL.md).

**Cancellation and timeout** — caller-driven abort, or a fixed timeout
with no setTimeout bookkeeping:

```javascript
const controller = new AbortController();
fetch(url, { signal: controller.signal }).catch((error) => {
  if (error.name === "AbortError") { /* expected on abort */ }
});
controller.abort();

const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
// throws TimeoutError after 5s
```

**Retry with exponential backoff:**

```javascript
async function fetchWithRetry(url, retries = 3, baseDelay = 500) {
  for (let attempt = 0; ; attempt++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (attempt >= retries - 1) throw error;
      await new Promise((r) => setTimeout(r, baseDelay * 2 ** attempt));
    }
  }
}
```

**Streaming large responses** — process chunks progressively instead of
buffering the full body:

```javascript
const reader = (await fetch("/large-file.json")).body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  processChunk(decoder.decode(value, { stream: true }));
}
```

## 13. Defensive Programming

Validate at boundaries, fail fast, never trust shape:

```javascript
if (!data || typeof data !== "object") return null;

const city = user?.address?.city;   // optional chaining
obj?.method?.();
const count = items?.length ?? 0;   // ?? falls back only on null/undefined

const button = document.querySelector(".btn-submit");
if (button) button.addEventListener("click", handleSubmit);
```

## 14. Storage

Web Storage holds strings only — always wrap values in JSON. Private
modes throw on access, so probe availability first. Never store secrets
or tokens in Web Storage — see [Auth](../auth/SKILL.md):

```javascript
localStorage.setItem("user", JSON.stringify({ name: "John" }));
const user = JSON.parse(localStorage.getItem("user") ?? "{}");
localStorage.removeItem("user");
sessionStorage.setItem("draft", "value"); // cleared when tab closes

function hasStorage() {
  try {
    localStorage.setItem("__t", "1");
    localStorage.removeItem("__t");
    return true;
  } catch {
    return false;
  }
}
```

**IndexedDB — use the `idb` wrapper.** For structured or large
client-side data use the `idb` package (tiny, promised, transaction-aware)
instead of hand-writing event-based wrappers around IndexedDB's callback
API:

```javascript
import { openDB } from "idb";

const db = await openDB("myapp", 1, {
  upgrade(db) { db.createObjectStore("items", { keyPath: "id" }); },
});
await db.put("items", { id: 1, name: "test" });
await db.get("items", 1);
```

## 15. Internationalization — Intl

Never hand-format dates, numbers or currency; Intl implements locale
rules. Create formatters once and reuse them — construction is costly:

```javascript
new Intl.DateTimeFormat("es-ES", {
  year: "numeric", month: "long", day: "numeric",
}).format(new Date()); // "24 de agosto de 2026"

new Intl.NumberFormat("es-ES", {
  style: "currency", currency: "EUR",
}).format(1234.56); // "1.234,56 €"
new Intl.NumberFormat("en-US", { style: "percent" }).format(0.75);

new Intl.RelativeTimeFormat("es", { numeric: "auto" }).format(-1, "day");
```

## 16. IntersectionObserver

Canonical viewport-detection pattern (reveal effects, lazy loading):

```javascript
const target = document.querySelector(".lazy-load");
if (!target) return;

const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      entry.target.classList.add("visible");
      observer.unobserve(entry.target); // fire once
    }
  },
  { rootMargin: "0px", threshold: 0.1 },
);
observer.observe(target);
```

## 17. Animation

CSS owns animation (transitions, keyframes — see
[CSS](../css/SKILL.md)). JavaScript enters only when CSS cannot express
the behavior (physics, gesture sync), via the Web Animations API. Animate
only `transform`/`opacity` — layout properties force reflow every frame
([Performance](../performance/SKILL.md)):

```javascript
const animation = element.animate(
  [{ opacity: 0, transform: "translateY(20px)" }, { opacity: 1 }],
  { duration: 300, easing: "ease-out", fill: "forwards" },
);
animation.cancel(); // release when finished or unmounted
```

## 18. Browser APIs

All of these are progressive enhancements: feature-detect before use.

**Clipboard** requires secure context; reading is permission-gated:

```javascript
await navigator.clipboard.writeText("Text to copy");
const text = await navigator.clipboard.readText();
```

**Web Share** delegates to the OS share sheet:

```javascript
if (navigator.share) {
  await navigator.share({ title: "My article", url: location.href });
}
```

**BroadcastChannel** — messaging between tabs of the same origin:

```javascript
const channel = new BroadcastChannel("app-updates");
channel.postMessage({ type: "LOGOUT" });          // sender tab
channel.addEventListener("message", (event) => {  // receiver tabs
  if (event.data.type === "LOGOUT") logout();
});
```

**WebSocket** connections must reconnect with exponential backoff:

```javascript
function connectWebSocket(url, { retries = 5, baseDelay = 1000 } = {}) {
  let attempt = 0;
  const ws = new WebSocket(url);
  ws.addEventListener("open", () => (attempt = 0));
  ws.addEventListener("message", (e) => showMessage(JSON.parse(e.data)));
  ws.addEventListener("close", () => {
    if (attempt >= retries) return;
    const delay = baseDelay * 2 ** attempt++; // 1s, 2s, 4s, ...
    setTimeout(() => connectWebSocket(url, { retries, baseDelay }), delay);
  });
  return ws;
}
```

**View Transitions (same-document)** smooth SPA state changes without
libraries. Guard the API, and inject only markup you control — replacing
`body` with fetched HTML containing user content is an XSS vector and
wipes listeners and state:

```javascript
if (!document.startViewTransition) return renderDirectly();

const transition = document.startViewTransition(async () => {
  document.body.innerHTML = await fetch(url)
    .then((r) => r.text()); // ⚠️ trusted, sanitized markup ONLY
});
await transition.finished;
```

**Popover lifecycle** — declarative setup belongs to HTML
([HTML](../html/SKILL.md)); JS reacts to state changes via `beforetoggle`:

```javascript
menu.addEventListener("beforetoggle", (event) => {
  if (event.newState === "open") focusFirstItem(menu);
});
```

**UUIDs and analytics beacons** — `crypto.randomUUID()` is Baseline
2023; `sendBeacon` survives page unload where `fetch` does not:

```javascript
const id = crypto.randomUUID();

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon("/api/analytics", JSON.stringify({ event: "exit" }));
  }
});
```

## 19. Performance

DOM-layer rules only. Read/write batching, layout-thrash avoidance and
measurement budgets are owned by the
[Performance skill](../performance/SKILL.md) — reference, don't duplicate.

**Style changes via classList** — per-property `style.*` writes each
trigger invalidation, and classes never clobber existing inline styles.
`style.cssText` REPLACES every inline style: only safe when you own all
of them.

```javascript
el.style.width = "100px";    // ❌ write-per-property
el.classList.add("is-open"); // ✅ one toggle, batched invalidation
```

**requestIdleCallback** defers non-urgent work until the browser is idle
(fallback: `setTimeout(drain, 0)`):

```javascript
function drain(deadline) {
  while (deadline.timeRemaining() > 0 && tasks.length > 0) tasks.shift()();
  if (tasks.length > 0) requestIdleCallback(drain, { timeout: 2000 });
}
requestIdleCallback(drain);
```

**requestAnimationFrame with delta time** — frame-rate-independent
animation for cases CSS/WAAPI cannot express:

```javascript
let last;
let position = 0;  // px
const SPEED = 0.1; // px per ms

function animate(timestamp) {
  position += SPEED * (timestamp - (last ?? timestamp));
  last = timestamp;
  el.style.transform = `translateX(${Math.min(position, 200)}px)`;
  if (position < 200) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
```

**Debounce vs throttle** — debounce waits for quiet (search inputs);
throttle caps the rate (scroll, resize):

```javascript
function debounce(fn, delay) {
  let id;
  return (...args) => {
    clearTimeout(id);
    id = setTimeout(() => fn(...args), delay);
  };
}
function throttle(fn, limit) {
  let ready = true;
  return (...args) => {
    if (!ready) return;
    ready = false;
    fn(...args);
    setTimeout(() => (ready = true), limit);
  };
}

input.addEventListener("input", debounce(search, 300));
window.addEventListener("scroll", throttle(onScroll, 100));
```

## 20. Common Dependencies

Vanilla JavaScript typically requires none. When a documented gap exists:

| Package  | Use                                        |
| -------- | ------------------------------------------ |
| date-fns | Date math/timezones beyond Intl formatting |
| idb      | Promised IndexedDB access (section 14)     |

With `structuredClone`, `Object.groupBy`, Set operations, iterator
helpers and the ES2023 array methods, lodash is no longer needed in most
vanilla projects.

## 21. Methodology

Before using any API or pattern not documented in this skill:

1. **MCP Context7 (priority)** — resolve the library/spec and query docs.
2. **MDN Web Docs** — confirm existence, Baseline status, browser support.
3. **Can I Use** — verify against the project's browserslist targets.
4. **Official spec** — TC39 proposals and WHATWG/W3C standards for edge cases.

**Hard rule:** if it is neither in this skill nor verifiable against two
authoritative sources, DO NOT USE IT. Document it as an assumption or
risk to the orchestrator.

## 22. Prohibitions

- No frameworks (Vue, Svelte, Angular) or React in vanilla projects
  without explicit authorization — see [React](../reactjs/SKILL.md)
- No TypeScript in vanilla projects unless required — see
  [TypeScript](../typescript/SKILL.md)
- No `var`, no undeclared globals, no leftover `console.log`/`debugger`
- No jQuery for selection or basic DOM work
- No JavaScript where CSS/HTML solve it (animation, tooltips, dialogs)
- No inline event handlers (`onclick=`) — bind with `addEventListener`
- No `innerHTML`/`insertAdjacentHTML` with user-controlled data (XSS)
- No `eval` or `new Function` (code injection)
- No `JSON.parse(JSON.stringify())` cloning — use `structuredClone()`
- No nested `Promise` constructors where `withResolvers()` fits
- No `keydown` Enter/Space handling on native `<button>`/`<a>`
  (they already fire `click`)
- No hand-rolled IndexedDB promise wrappers — use `idb`
- No unverified or experimental APIs — pass the Methodology gate first

## 23. References

> Structure, forms, declarative components: [HTML](../html/SKILL.md)
> Styling, transitions, scroll effects: [CSS](../css/SKILL.md)
> Focus and keyboard policy: [Accessibility](../accessibility/SKILL.md)
> Layout thrash, batching, CWV: [Performance](../performance/SKILL.md)
> CSP, XSS sanitization, injection: [Security](../security/SKILL.md)
> Contracts, status codes, pagination: [API Design](../api-design/SKILL.md)
> Tokens, cookies, credential storage: [Auth](../auth/SKILL.md)
> Type safety and gradual typing: [TypeScript](../typescript/SKILL.md)
> Component architectures: [React](../reactjs/SKILL.md)
> Hosting and CI/CD: [Deploy](../deploy/SKILL.md)

Removed in v2.0.0 (audit): WeakRef/FinalizationRegistry, Array.fromAsync,
`JSON.parse` context.source, Uint8Array base64/hex, File System Access
API, the layout-thrash example (owned by Performance) and the hand-rolled
IndexedDB wrapper. History: git and `.backup/SKILL.md.bak`.

Last updated: 2026-08

