What can be checked automatically
Part of this catalog is already enforced by a lint rule, so it belongs in CI rather than in a review. The rest is multi-line behaviour spread over a file, which no linter can match and a person or an agent has to recognise. Knowing which half a migration falls into is most of deciding how to tackle it.
8 rules a linter already finds
Turn these on and stop reviewing them by hand. This catalog names the rule rather than reimplementing the check, the same way it points at guides rather than copying them.
- Deep cloning
structuredClone() - Deferred promises
Promise.withResolvers() - Element resize tracking
ResizeObserver - Event emitters
EventTarget with CustomEvent - Grouping a list by key
Object.groupBy() and Map.groupBy() - Query string parsing
URLSearchParams and the URL constructor - Timing out async work
AbortSignal.timeout() - Visibility tracking
IntersectionObserver
All of them at once, in an eslint config:
import unicorn from "eslint-plugin-unicorn";
export default [
{
plugins: { unicorn },
rules: {
"unicorn/prefer-abort-signal-timeout": "error",
"unicorn/prefer-event-target": "error",
"unicorn/prefer-group-by": "error",
"unicorn/prefer-observer-apis": "error",
"unicorn/prefer-observer-apis": "error",
"unicorn/prefer-promise-with-resolvers": "error",
"unicorn/prefer-structured-clone": "error",
"unicorn/prefer-url-search-parameters": "error",
},
},
];114 shapes that need a person or a model
No package is installed for any of these, so nothing matches in a package.json either. They are stateful and spread over a file, which is why no lint rule covers them. An agent reading the code can, which is what the skill and the MCP server are for.
- Accordions
<details name> for an exclusive accordion - Animating from JavaScript
element.animate() - Animating to height auto
interpolate-size: allow-keywords, or calc-size() - Auto-growing textareas
field-sizing: content - Balanced headings
text-wrap: balance - Base64 encoding
btoa() and atob(), with TextEncoder for text - Cancelling async work
AbortController and AbortSignal - Carousels
CSS scroll-snap with ::scroll-button() and ::scroll-marker() - Choosing a plural form
Intl.PluralRules - Colour manipulation
color-mix(), oklch() and relative colour syntax - Copy and paste
navigator.clipboard.writeText() - Country and language names
Intl.DisplayNames - Cross-tab messaging
BroadcastChannel - Custom select menus
appearance: base-select on <select> - Date and time pickers
<input type="date"> and <input type="time"> - Date arithmetic and time zones
Temporal - Element resize tracking
ResizeObserver - Element size queries
Container queries - Enter and exit transitions
@starting-style with transition-behavior: allow-discrete - File pickers and drop zones
<input type="file"> with drop events - Fixed aspect ratios
aspect-ratio - Focus trapping
the inert attribute - Form validation
constraint validation with :user-invalid - Fullscreen toggling
Element.requestFullscreen() - Generating UUIDs
crypto.randomUUID() - Hashing and encryption
crypto.subtle - HTTP requests
fetch() - Human-readable durations
Intl.DurationFormat - Joining a list into a sentence
Intl.ListFormat - jQuery
querySelectorAll(), classList and addEventListener() - Keeping the screen awake
navigator.wakeLock.request("screen") - Keyboard-only focus styling
:focus-visible - Knowing when a webfont has loaded
document.fonts with font-display - Lazy-loaded images and iframes
loading="lazy" - Light and dark theming
light-dark() with color-scheme - Locale-aware date and time formatting
Intl.DateTimeFormat - Locale-aware number and currency formatting
Intl.NumberFormat - Masonry layouts
CSS masonry item placement - Matching URL patterns
URLPattern - Modal dialogs
<dialog> with showModal() - Multi-line text truncation
-webkit-line-clamp - Mutexes and critical sections
navigator.locks.request() - Natural and locale-aware sorting
Intl.Collator with numeric: true - Passkeys in the browser
navigator.credentials with PublicKeyCredential - Prefetching the next page
speculation rules - Reacting to a breakpoint
matchMedia() - Reading and writing cookies
cookieStore - Relative time formatting
Intl.RelativeTimeFormat - Reordering a list by dragging
draggable and the drag events - Resizable panels
resize - Saving a generated file
a Blob object URL on a download link - Scroll chaining and body scroll lock
overscroll-behavior: contain - Scroll-triggered animations
animation-timeline: view() and scroll() - Scrolling an element into view
scrollIntoView({ block: "nearest" }) - Server-sent events
EventSource - Sharing to other apps
navigator.share() - Smooth scrolling and scroll-to-anchor
scroll-behavior: smooth with scroll-margin-top - Spinners and progress bars
<progress> - Splitting text by character or word
Intl.Segmenter - Sticky headers and sidebars
position: sticky - Stripping accents from text
String.prototype.normalize("NFD") - Styled scrollbars
scrollbar-width, scrollbar-color and scrollbar-gutter - Tab visibility
document.visibilityState and the visibilitychange event - Tooltips, dropdowns and popovers
The Popover API with CSS anchor positioning - Touch and pointer gestures
pointer events with touch-action
Not this tool's job
Three questions near this one, each better answered somewhere else. Reimplementing them here would be worse than pointing at them.
- knip
- Unused dependencies, exports and files. It reads npm scripts, config files and plugin names, which is what makes an "unused" claim trustworthy rather than a guess.
- Biome useBaseline
- The opposite direction to this catalog. It stops you using a feature before your target browsers have it, where this tells you a feature is finally old enough to replace a library.
- eslint-plugin-compat
- Checks calls against your browserslist. Useful alongside a finding here that reads as newly available.