---
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