Home Benchmarks Learn Tools News
SPONSOR

AppSignal — Stop vibe-debugging. Every exception, every backtrace, grouped so you see patterns, not noise.

↗
Skills · Components
Front-end skill · v2 · 9 patterns

Components, not divs.

Runs inside your agent. Replaces <div onclick> soup with native elements, ARIA patterns, and BEM you can override.

Install the skill → Read SKILL.md
9
Native patterns
6
AI tools supported
0
Dependencies
agent — frontend-components live
$ agent write --skill frontend-components Modal.tsx
▼ generating Modal.tsx with skill loaded
<dialog> for modal (focus trap built-in) USED
Escape closes (no JS handler) USED
<form method="dialog"> for buttons USED
heading order valid (h2 in header) USED
aria-labelledby links to title USED
<button> over <div onclick> USED
no role="button" on <button> CLEAN
written Modal.tsx · 0 anti-patterns A
→ ready to ship. open in editor? [Y/n]
Works with
  • Cursor
  • Claude Code
  • Codex CLI
  • Windsurf
  • GitHub Copilot
  • Gemini CLI
What it enforces

Native elements, real semantics.

frontend-components / patterns SKILL.md · native · baseline 2026
patterns/
buttons.diff •
−Actions.tsx Before
<div className="btn" onClick={handleClick}>Submit</div>
 
<a href="#" onClick={openModal}>Open</a>
 
<span onClick={toggle}>Menu</span>
+Actions.tsx After
<button type="submit">Submit</button>
 
<button type="button" onClick={openModal}>Open</button>
 
<button type="button"
aria-expanded={open}
onClick={toggle}>Menu</button>
− no keyboard · no role · no focus → + Enter, Space, Tab, focus ring · all free
dialog.diff •
−Modal.tsx Before
<div className="modal-overlay">
<div className="modal" role="dialog">
<h2>Confirm</h2>
<button onClick={onCancel}>Cancel</button>
<button onClick={onConfirm}>Confirm</button>
</div>
</div>
// + 50 lines of focus trap, Escape, scroll-lock JS
+Modal.tsx After
<dialog ref={dialogRef}>
<form method="dialog">
<h2>Confirm</h2>
<menu>
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</menu>
</form>
</dialog>
// dialogRef.current.showModal() — everything else: free
− 50+ lines of focus-trap, Escape, scroll-lock JS → + showModal() · the platform handles the rest
disclosure.diff •
−Disclosure.tsx Before
<button onClick={() => setOpen(!open)}
aria-expanded={open}>FAQ</button>
{open && <div className="answer">Answer</div>}
 
<button onClick={openMenu}>Menu</button>
<ul className={menuOpen ? 'open' : ''}>...</ul>
// + click-outside listener, Escape handler
+Disclosure.tsx After
<details>
<summary>FAQ</summary>
<p>Answer</p>
</details>
 
<button popovertarget="menu">Menu</button>
<div id="menu" popover>
<ul>...</ul>
</div>
// light-dismiss + Escape + top-layer: free
− state hooks · click-outside · Escape handler → + one element · Popover API does the rest
aria.diff •
−controls.tsx Before
<button role="button" aria-label="Submit">Submit</button>
 
<nav role="navigation">...</nav>
 
<div className="tab" onClick={selectTab}>Profile</div>
+controls.tsx After
<button>Submit</button>
 
<nav>...</nav>
 
<button role="tab"
aria-selected={active}
aria-controls="panel-1"
tabindex={active ? 0 : -1}>Profile</button>
− roles duplicate native semantics · missing states → + native first · ARIA only fills the gaps
Benchmarked

Proof, not vibes.

Pricing Section brief · Claude Opus 4.7 · with skill vs. without · 2026-04-21
100
Best Practices 78 → 100 baseline +22
96
Accessibility 84 → 96 baseline +12
8
Native elements used Avg adopted per file
93
Components grade · A vs C · 72 baseline +21
0–49 50–89 90–100
Single-run comparison. Same model and prompt, scored on Lighthouse + an internal rubric.
Install

One file. Six tools. Zero ceremony.

One Markdown file, zero dependencies. Pick your tool below.

1Drop this in

Project: .cursor/skills/frontend-components.md

2Or fetch it directly
curl -fsSL https://webdeveloper.com/skills/frontend-components/SKILL.md -o .cursor/skills/frontend-components.md

Restart Cursor. Every component the agent writes uses semantic HTML and avoids the listed anti-patterns.

1Drop this in

User-level: ~/.claude/skills/frontend-components/SKILL.md

2Or fetch it directly
mkdir -p ~/.claude/skills/frontend-components && curl -fsSL https://webdeveloper.com/skills/frontend-components/SKILL.md -o ~/.claude/skills/frontend-components/SKILL.md

Claude Code auto-discovers skills in ~/.claude/skills/. Available across every project on this machine.

1Drop this in

Project: AGENTS.md (append the SKILL contents)

2Or fetch it directly
curl -fsSL https://webdeveloper.com/skills/frontend-components/SKILL.md >> AGENTS.md

Codex CLI reads AGENTS.md automatically when you run it from the project root.

1Drop this in

Project: .windsurf/rules/frontend-components.md

2Or fetch it directly
mkdir -p .windsurf/rules && curl -fsSL https://webdeveloper.com/skills/frontend-components/SKILL.md -o .windsurf/rules/frontend-components.md

Windsurf loads project rules on every Cascade run.

1Drop this in

Project: .github/copilot-instructions.md (append)

2Or fetch it directly
mkdir -p .github && curl -fsSL https://webdeveloper.com/skills/frontend-components/SKILL.md >> .github/copilot-instructions.md

Copilot reads .github/copilot-instructions.md as project-wide context.

1Drop this in

Project: .gemini/skills/frontend-components.md

2Or fetch it directly
mkdir -p .gemini/skills && curl -fsSL https://webdeveloper.com/skills/frontend-components/SKILL.md -o .gemini/skills/frontend-components.md

Gemini CLI auto-loads project skills on the next run.

The full SKILL.md

519 lines · plain Markdown · MIT-licensed
SKILL.md
---
name: frontend-components
description: >-
  Enforces semantic HTML, proper ARIA patterns, and component architecture when
  building UI. Use when generating HTML markup, creating interactive components,
  building forms, adding modals or dialogs, or structuring page content.
---

# Semantic HTML and Component Architecture

Build components with correct HTML semantics and native interactive elements.
The goal is markup that is meaningful to browsers, assistive technologies, and
search engines — not `<div>` soup with classes.

This skill covers what to build. See the `frontend-accessibility` skill for
how to verify it works for all users (contrast, keyboard, screen reader
testing).

## Element Selection

Choose the most specific semantic element available. Only fall back to `<div>`
or `<span>` when no semantic element fits the content.

**The decision is simple**: if the element has a semantic meaning that matches
your content, use it. If you're reaching for a `<div>`, ask what the content
*is* — there's usually a better element.

### Sectioning

| Element | When to use | Not this |
|---------|-------------|----------|
| `<header>` | Introductory content for its nearest sectioning ancestor | `<div class="header">` |
| `<nav>` | Major navigation blocks (main nav, footer nav, breadcrumbs) | `<div class="nav">` |
| `<main>` | Primary content — exactly one per page | `<div class="content">` |
| `<article>` | Self-contained content (blog post, card, comment) | `<div class="post">` |
| `<section>` | Thematic grouping with a heading | `<div class="section">` |
| `<aside>` | Tangentially related content (sidebar, pull quote) | `<div class="sidebar">` |
| `<footer>` | Footer for its nearest sectioning ancestor | `<div class="footer">` |

`<section>` vs `<div>`: if the grouping has a heading and the content is
thematically related, use `<section>`. If it's purely a styling wrapper,
use `<div>`. A `<section>` without a heading is usually wrong.

### Text and Data

| Element | When to use |
|---------|-------------|
| `<h1>`–`<h6>` | Headings that establish document outline |
| `<p>` | Paragraphs of text |
| `<ul>` / `<ol>` | Lists (`<ul>` when order doesn't matter, `<ol>` when it does) |
| `<dl>` | Key-value pairs, metadata, glossaries, specs |
| `<time>` | Dates and times — include `datetime` attribute |
| `<address>` | Contact info for the nearest `<article>` or `<body>` |
| `<blockquote>` | Extended quotations — include `<cite>` for attribution |
| `<figure>` / `<figcaption>` | Self-contained media with a caption |
| `<code>` / `<pre>` | Inline code / preformatted code blocks |
| `<mark>` | Highlighted or referenced text |
| `<abbr>` | Abbreviations — include `title` with expansion |

### Interactive

| Element | When to use |
|---------|-------------|
| `<a>` | Navigation to a URL — never `<div onclick>` |
| `<button>` | Actions that don't navigate — toggles, submissions, triggers |
| `<details>` / `<summary>` | Disclosure widget (expand/collapse) — no JS needed |
| `<dialog>` | Modal and non-modal dialogs — use `.showModal()` in JS |
| `<input>` / `<select>` / `<textarea>` | Form controls |
| `<output>` | Result of a calculation or user action |

**The `<a>` vs `<button>` rule**: if clicking it changes the URL, use `<a>`.
If clicking it does something on the current page, use `<button>`. There are
no exceptions. A `<div>` with a click handler is never acceptable.

## Heading Hierarchy

- Exactly one `<h1>` per page matching the page topic
- Never skip heading levels (`<h1>` then `<h3>`)
- Headings establish document outline — don't choose level for font size;
  use CSS for visual sizing
- Every `<section>` should have a heading (use `aria-label` if the heading
  is visually hidden)
- The `<h1>` should align with the page's `<title>` keyword (see
  `frontend-seo` skill)

```html
<h1>Article Title</h1>
  <h2>First Section</h2>
    <h3>Subsection</h3>
  <h2>Second Section</h2>
    <h3>Subsection</h3>
      <h4>Detail</h4>
```

## Native Interactive Elements

Always prefer native elements over custom implementations. Native elements
come with keyboard handling, focus management, and screen reader support for
free. Custom implementations require manually reimplementing all of that.

### Dialog

```html
<dialog id="confirm-dialog">
  <form method="dialog">
    <h2>Confirm Action</h2>
    <p>Are you sure you want to proceed?</p>
    <menu>
      <button value="cancel">Cancel</button>
      <button value="confirm">Confirm</button>
    </menu>
  </form>
</dialog>
```

```javascript
const dialog = document.getElementById('confirm-dialog');
dialog.showModal();
dialog.addEventListener('close', () => {
  console.log(dialog.returnValue);
});
```

What `<dialog>` with `.showModal()` gives you for free: focus trapping,
`Escape` to close, `::backdrop` styling, `inert` on background content,
and correct screen reader announcements. A custom `<div>` modal requires
manually implementing all of this and will likely have bugs.

### Details / Summary

Use for disclosure widgets (FAQs, accordions, collapsible sections):

```html
<details>
  <summary>How does billing work?</summary>
  <p>We charge monthly on the date you signed up.</p>
</details>
```

For exclusive accordion behavior (only one open at a time), use the `name`
attribute:

```html
<details name="faq">
  <summary>Question one</summary>
  <p>Answer one.</p>
</details>
<details name="faq">
  <summary>Question two</summary>
  <p>Answer two.</p>
</details>
```

### Popover API

Use for tooltips, dropdowns, and non-modal overlays:

```html
<button popovertarget="menu">Options</button>
<div id="menu" popover>
  <ul role="menu">
    <li role="menuitem"><button>Edit</button></li>
    <li role="menuitem"><button>Delete</button></li>
  </ul>
</div>
```

Popover API provides: light-dismiss (click outside closes), top-layer
rendering (no z-index needed), `Escape` to close. Unlike `<dialog>`, it does
not trap focus or add `inert` to the background — it's for non-modal content.

### When to use each

| Pattern | Element | Focus trapped? | Backdrop? | Light-dismiss? |
|---------|---------|---------------|-----------|----------------|
| Modal dialog | `<dialog>` + `.showModal()` | Yes | Yes | No (intentional) |
| Non-modal dialog | `<dialog>` + `.show()` | No | No | No |
| Dropdown / tooltip | `[popover]` | No | No | Yes |
| Expand/collapse | `<details>` | No | No | No |

## ARIA Widget Patterns

Use ARIA only when native HTML doesn't provide the interaction pattern.
Follow the WAI-ARIA Authoring Practices 1.2.

**The first rule of ARIA**: don't use ARIA if a native HTML element provides
the same semantics. `role="button"` on a `<div>` is always wrong when
`<button>` exists.

### Tabs

```html
<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
    Profile
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2"
          tabindex="-1">
    Security
  </button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  <!-- Profile content -->
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  <!-- Security content -->
</div>
```

Keyboard: `Arrow Left`/`Right` between tabs (roving tabindex), `Tab` into
panel. Active tab: `tabindex="0"`. Inactive tabs: `tabindex="-1"`. See the
`frontend-accessibility` skill for roving tabindex details.

### Combobox (Autocomplete)

```html
<label for="search">Search</label>
<div role="combobox" aria-expanded="false" aria-haspopup="listbox">
  <input id="search" type="text" aria-autocomplete="list"
         aria-controls="results">
  <ul id="results" role="listbox" hidden>
    <li role="option" id="opt-1">Result one</li>
    <li role="option" id="opt-2">Result two</li>
  </ul>
</div>
```

Set `aria-activedescendant` on the input to the currently highlighted
option's ID. Update `aria-expanded` when the listbox opens/closes.

### Live Regions

For dynamic content that updates without a page reload (notifications,
search results count, form validation):

```html
<div aria-live="polite" aria-atomic="true" class="visually-hidden">
  3 results found
</div>
```

- `polite` — announces after current speech (status updates)
- `assertive` — interrupts immediately (errors, urgent alerts only)
- `aria-atomic="true"` — reads the entire region, not just the changed part

## Forms

```html
<form novalidate>
  <div class="form-group">
    <label for="email">Email address</label>
    <input id="email" type="email" required aria-describedby="email-hint">
    <p id="email-hint" class="form-hint">We'll never share your email.</p>
  </div>

  <fieldset>
    <legend>Notifications</legend>
    <label><input type="checkbox" name="notify" value="email"> Email</label>
    <label><input type="checkbox" name="notify" value="sms"> SMS</label>
  </fieldset>

  <button type="submit">Subscribe</button>
</form>
```

- Every `<input>` must have an associated `<label>` (via `for`/`id` or
  wrapping)
- Group related controls with `<fieldset>` + `<legend>`
- Use `aria-describedby` for hints and error messages
- Use `aria-invalid="true"` on invalid fields
- Use `novalidate` on `<form>` when doing custom JS validation
- See the `frontend-accessibility` skill for error handling patterns

## Progressive Enhancement

Build markup that works without JavaScript. Layer interactivity on top.

1. Links navigate, forms submit, disclosure widgets toggle — all without JS
2. JS adds smoother transitions, client validation, and dynamic updates
3. Use `<noscript>` only when a JS-dependent feature has no fallback

## Data Attributes for JavaScript

Use `data-*` attributes to connect JS behavior to elements. Never use CSS
classes as JS hooks — it creates a fragile coupling between styling and
behavior.

```html
<button data-action="toggle-menu" data-target="main-nav">Menu</button>
```

```javascript
document.querySelectorAll('[data-action="toggle-menu"]').forEach(btn => {
  btn.addEventListener('click', () => {
    const target = document.getElementById(btn.dataset.target);
    target.toggleAttribute('hidden');
  });
});
```

Naming: `data-action` for what it does, `data-target` for what it acts on,
`data-state` for current state. Prefix with component name for complex
components: `data-carousel-slide`, `data-carousel-index`.

## BEM Naming Convention

Use Block-Element-Modifier naming for CSS classes:

```html
<article class="card">
  <header class="card__header">
    <span class="card__tag">Category</span>
  </header>
  <div class="card__body">
    <h3 class="card__title">Title</h3>
    <p class="card__excerpt">Description.</p>
  </div>
  <footer class="card__footer">
    <a href="#" class="btn btn--primary">Read more</a>
  </footer>
</article>
```

- **Block**: standalone component (`.card`, `.btn`, `.nav`)
- **Element**: part of a block, `__` prefix (`.card__title`)
- **Modifier**: variant or state, `--` prefix (`.btn--primary`)
- Never chain elements (`.card__header__title`) — flatten to `.card__title`
- Modifiers go on the same element as the block or element they modify

## Anti-Patterns

**Never do these:**

- Use `<div>` or `<span>` with a click handler instead of `<button>`
  or `<a>` — divs have no keyboard handling, no focus, no role
- Use `<a>` without `href` or with `href="#"` for actions — use `<button>`
- Skip heading levels (`<h1>` then `<h3>`) — breaks document outline for
  screen readers and SEO
- Build a modal with `<div>` instead of `<dialog>` — you'll miss focus
  trapping, Escape handling, backdrop, and inert
- Add ARIA roles that duplicate native semantics (`role="button"` on a
  `<button>`) — it's redundant and can cause double announcements
- Nest interactive elements (`<a>` inside `<button>`) — creates ambiguous
  behavior for keyboards and screen readers
- Use `<br>` for spacing — use CSS margin or padding
- Use `<table>` for layout — only for tabular data
- Put block elements inside `<p>` or `<span>` — invalid HTML that browsers
  will auto-correct unpredictably
- Use CSS classes as JavaScript hooks — use `data-*` attributes
Pair it

Stack it with the rest of the suite.

Front-end06 Accessibility

WCAG 2.2 AA, keyboard nav, focus management, contrast, screen reader patterns, form accessibility.

↗
Front-end01 Modern CSS

Native nesting, container queries, :has(), scroll-driven animations, oklch colors, logical properties, and CSS layers.

↗
Front-end04 Design Tokens

Two-tier token architecture, spacing and type scales, oklch color system, dark/light theming, motion tokens, and z-index scales.

↗

Changelog

V2 April 9, 2026
Added element decision rationale, dialog/popover/details comparison table, data-* attributes section for JS hooks, cross-references to Accessibility and SEO skills. Sharpened anti-patterns with reasoning.
V1 April 3, 2026
Initial skill covering semantic HTML elements, heading hierarchy, native dialog, Popover API, ARIA widget patterns, forms, progressive enhancement, and BEM naming.

FAQ

What HTML component patterns does this skill enforce?

Semantic HTML element selection, native dialog for modals, the Popover API for tooltips and dropdowns, details/summary for disclosure widgets, ARIA widget patterns for tabs, comboboxes, and live regions, proper form markup with associated labels, and BEM naming conventions for CSS classes.

How does this skill handle accessibility in components?

Accessibility is built into every pattern. The skill requires associated labels for all form inputs, proper ARIA roles and states for interactive widgets, keyboard navigation with roving tabindex, focus management for dialogs and popovers, and heading hierarchy that establishes a meaningful document outline.

Does this skill work with React or Vue?

Yes. The skill teaches universal HTML and ARIA patterns that apply regardless of framework. Whether you are writing plain HTML, React JSX, Vue templates, or any other framework, the semantic patterns and accessibility requirements are the same.

Which AI coding tools is this compatible with?

Cursor, Claude Code, Codex CLI, Windsurf, GitHub Copilot, and Gemini CLI. The skill is a single Markdown file and ships in the native format for each tool with one-click copy.

STATUS ● BUILDING THE FUTURE
MISSION MAKE AI SHIP BETTER CODE.
VERSION BETA 3.0

MAKE AI SHIP BETTER CODE.

@WEBDEVELOPERHQ ↗
TERMS / PRIVACY
FRIENDS
Authentic Jobs ↗
Web Reference ↗
Ready.dev ↗
Fullres ↗
© 2026 WEB DEVELOPER / ALL RIGHTS RESERVED