Automated summary
Guides HTML-first feature development that remains usable without JavaScript by relying on native elements, CSS pseudo-classes, and APIs such as View Transitions.
Strengths
- Provides concrete no-JavaScript patterns for <details>, <dialog>, and popover, including command/commandfor attributes.
- Clarifies the trade-offs between modal dialogs and popovers, such as blocking, focus handling, and light dismiss.
- Includes reusable CSS examples for styling, entry animations, and open/closed states.
- Promotes an accessible, crawlable HTML baseline before adding CSS or JavaScript.
Limitations
- Assumes modern browser support for command/commandfor, @starting-style, and allow-discrete without documenting fallbacks.
- Focuses on markup and CSS; it does not cover JavaScript fallback strategies for complex interactions.
- The supplied content is incomplete—the command table cuts off—and does not include a concrete View Transitions example.
Use this skill when building simple UI components—accordions, modals, menus—that should work without JavaScript or degrade gracefully with CSS enhancement.
Avoid it for highly stateful or real-time interactions that fundamentally require complex JavaScript and broad browser compatibility.
Security analysis
SafeThe skill only uses Read, Write, and Edit tools. It provides instructional HTML/CSS patterns and a small JavaScript snippet for custom commands, but does not execute any code or issue commands. No destructive actions, network calls, or data exfiltration are present.
No concerns found
How this analysis is madeExamples
Build a FAQ section using only <details> and <summary> elements, styled with CSS. It must work with JavaScript disabled and keep the markup accessible.Create a settings modal with the native <dialog> element and declarative command/commandfor buttons so it opens and closes without JavaScript. Include focus and backdrop styling.Implement a responsive user menu with the Popover API and CSS entry animations, using toggle-popover/show-popover/hide-popover commands instead of JavaScript.name: progressive-enhancement description: HTML-first development with CSS-only interactivity patterns. Use when building features that work without JavaScript, using native HTML elements, CSS pseudo-classes, and the View Transitions API. allowed-tools: Read, Write, Edit
Progressive Enhancement Skill
This skill covers HTML-first development patterns where functionality works without JavaScript. CSS provides visual feedback and interactivity through pseudo-classes, the :has() selector, and native HTML elements.
Philosophy
Build in layers:
Layer 1: HTML → Works in any browser, screen readers, search engines
Layer 2: CSS → Layout, theming, validation feedback, animations
Layer 3: JS (opt) → Enhanced interactions (only when truly necessary)
Every feature should have a baseline HTML-only experience that CSS enhances.
Native HTML Elements for Interactivity
<details> and <summary> for Accordions
No JavaScript needed for expand/collapse:
<details>
<summary>What is your return policy?</summary>
<p>We offer a 30-day hassle-free return policy for all unused items.</p>
</details>
details {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--spacing-md);
& summary {
cursor: pointer;
font-weight: var(--font-weight-semibold);
&::marker {
color: var(--primary);
}
}
&[open] summary {
margin-bottom: var(--spacing-md);
}
}
<dialog> for Modals
Native modal with backdrop and focus trapping—fully declarative with command/commandfor:
<button commandfor="settings-dialog" command="show-modal">
Open Settings
</button>
<dialog id="settings-dialog">
<h2>Settings</h2>
<form method="dialog">
<!-- Form content -->
<button type="submit">Close</button>
<!-- Or explicit close button -->
<button commandfor="settings-dialog" command="close">Cancel</button>
</form>
</dialog>
dialog {
border: none;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
max-width: 32rem;
padding: var(--spacing-lg);
&::backdrop {
background: oklch(0% 0 0 / 0.5);
}
}
No JavaScript required. The browser handles:
- Opening/closing
- Focus management
aria-expandedstates- Backdrop clicks (with
method="dialog")
Popover API
Popovers provide lightweight, non-modal overlays:
<button commandfor="user-menu" command="toggle-popover">
Menu
</button>
<div popover id="user-menu">
<nav>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<a href="/logout">Logout</a>
</nav>
</div>
[popover] {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--spacing-md);
box-shadow: var(--shadow-md);
/* Entry animation */
opacity: 0;
transform: translateY(-0.5rem);
transition: opacity 0.2s, transform 0.2s, display 0.2s allow-discrete;
&:popover-open {
opacity: 1;
transform: translateY(0);
}
/* Starting style for animation */
@starting-style {
&:popover-open {
opacity: 0;
transform: translateY(-0.5rem);
}
}
}
Popover Commands
| Command | Behavior |
|---------|----------|
| toggle-popover | Toggle open/closed |
| show-popover | Open only |
| hide-popover | Close only |
Dialog vs Popover
| Feature | <dialog> | popover |
|---------|-----------|-----------|
| Modal blocking | Yes (with show-modal) | No |
| Backdrop | Yes | Optional |
| Light dismiss | With method="dialog" | Yes (click outside) |
| Focus trapping | Yes | No |
| Use case | Confirmations, forms | Menus, tooltips, dropdowns |
The command and commandfor Attributes
These attributes enable declarative button behaviors without JavaScript:
<button commandfor="target-id" command="action">Label</button>
Built-in Commands
| Command | Target | Effect |
|---------|--------|--------|
| show-modal | <dialog> | Opens as modal |
| close | <dialog> | Closes dialog |
| toggle-popover | [popover] | Toggles popover |
| show-popover | [popover] | Opens popover |
| hide-popover | [popover] | Closes popover |
Custom Commands
Define custom behaviors with -- prefix:
<button commandfor="gallery" command="--next-slide">Next</button>
<button commandfor="gallery" command="--prev-slide">Previous</button>
<div id="gallery">
<!-- Gallery content -->
</div>
// JavaScript handles custom commands
document.getElementById('gallery').addEventListener('command', (event) => {
switch (event.command) {
case '--next-slide':
// Next slide logic
break;
case '--prev-slide':
// Previous slide logic
break;
}
});
Benefits Over onclick
| onclick | command/commandfor |
|-----------|------------------------|
| Inline JavaScript | Declarative HTML |
| CSP violations possible | CSP-friendly |
| Manual aria management | Automatic accessibility |
| Framework state needed | Browser handles state |
<datalist> for Autocomplete
<label for="browser">Choose a browser:</label>
<input list="browsers" id="browser" name="browser"/>
<datalist id="browsers">
<option value="Chrome"/>
<option value="Firefox"/>
<option value="Safari"/>
<option value="Edge"/>
</datalist>
CSS-Only Interactivity Patterns
The Checkbox Hack
Use hidden checkboxes to toggle state without JavaScript:
<input type="checkbox" id="menu-toggle" hidden/>
<label for="menu-toggle" data-menu-trigger>Menu</label>
<nav data-mobile-nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
nav[data-mobile-nav] {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
#menu-toggle:checked ~ nav[data-mobile-nav] {
max-height: 500px;
}
/* Toggle icon state */
[data-menu-trigger]::before {
content: "☰";
}
#menu-toggle:checked ~ [data-menu-trigger]::before {
content: "✕";
}
Theme Toggle with Radio Buttons
<fieldset data-theme-selector>
<legend>Theme</legend>
<label>
<input type="radio" name="theme" id="theme-light" checked/>
Light
</label>
<label>
<input type="radio" name="theme" id="theme-dark"/>
Dark
</label>
<label>
<input type="radio" name="theme" id="theme-auto"/>
Auto
</label>
</fieldset>
:root {
--bg: white;
--text: #1f2937;
}
:root:has(#theme-dark:checked) {
--bg: #1f2937;
--text: #f9fafb;
}
:root:has(#theme-auto:checked) {
@media (prefers-color-scheme: dark) {
--bg: #1f2937;
--text: #f9fafb;
}
}
body {
background: var(--bg);
color: var(--text);
}
Tab Panels with Radio Buttons
<div data-tabs>
<input type="radio" name="tabs" id="tab-1" checked hidden/>
<input type="radio" name="tabs" id="tab-2" hidden/>
<input type="radio" name="tabs" id="tab-3" hidden/>
<nav>
<label for="tab-1">Overview</label>
<label for="tab-2">Features</label>
<label for="tab-3">Pricing</label>
</nav>
<section data-tab="1">Overview content...</section>
<section data-tab="2">Features content...</section>
<section data-tab="3">Pricing content...</section>
</div>
[data-tabs] {
& section {
display: none;
}
&:has(#tab-1:checked) section[data-tab="1"],
&:has(#tab-2:checked) section[data-tab="2"],
&:has(#tab-3:checked) section[data-tab="3"] {
display: block;
}
& nav label {
cursor: pointer;
padding: var(--spacing-sm) var(--spacing-md);
}
&:has(#tab-1:checked) label[for="tab-1"],
&:has(#tab-2:checked) label[for="tab-2"],
&:has(#tab-3:checked) label[for="tab-3"] {
background: var(--primary);
color: white;
}
}
The :has() Selector
The :has() selector enables parent-based styling:
Conditional Parent Styling
/* Style parent when it contains a specific child */
article:has(img) {
display: grid;
grid-template-columns: 1fr 2fr;
}
/* Form field styling based on input state */
form-field:has(input:required) label::after {
content: " *";
color: var(--error);
}
/* Card highlight when checkbox is checked */
product-card:has(input:checked) {
border-color: var(--primary);
}
Sibling-Based Styling
/* Style elements when sibling checkbox is checked */
input:checked + label {
font-weight: bold;
}
/* Style nav based on hidden checkbox state */
#nav-toggle:checked ~ nav {
max-height: 500px;
}
Form Validation Pseudo-Classes
:user-valid and :user-invalid
These apply only after user interaction, preventing premature error states:
/* Only show valid state after user has typed */
input:user-valid {
border-color: var(--success);
}
/* Only show error after user has interacted */
input:user-invalid {
border-color: var(--error);
}
/* Parent styling based on input state */
form-field:has(input:user-valid) output {
color: var(--success);
}
form-field:has(input:user-invalid) output {
color: var(--error);
}
:valid and :invalid (Immediate)
These apply immediately, even before interaction:
/* Use for optional "preview" validation */
input:valid {
/* Subtle indication */
}
input:invalid {
/* Avoid strong error styling - user hasn't finished */
}
:placeholder-shown
Style based on whether input has content:
input:placeholder-shown {
/* Empty input styles */
}
input:not(:placeholder-shown) {
/* Has content */
}
:required and :optional
input:required {
/* Required field indicator */
}
label:has(+ input:required)::after {
content: " *";
color: var(--error);
}
View Transitions API
Smooth page-to-page animations without JavaScript:
Enable View Transitions
<meta name="view-transition" content="same-origin"/>
Default Transition
With the meta tag, all same-origin navigations get a crossfade by default.
Named Transitions
/* Assign transition names */
.hero-image {
view-transition-name: hero;
}
/* Or use data attributes */
[data-vt="card-1"] {
view-transition-name: card-1;
}
/* Custom animation */
::view-transition-old(hero) {
animation: fade-out 0.3s ease-out;
}
::view-transition-new(hero) {
animation: fade-in 0.3s ease-in;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
Card-to-Detail Transition
<!-- List page -->
<article data-vt="product-123">
<img src="product.jpg" style="view-transition-name: product-123-img"/>
<h2 style="view-transition-name: product-123-title">Product Name</h2>
</article>
<!-- Detail page -->
<article>
<img src="product.jpg" style="view-transition-name: product-123-img"/>
<h1 style="view-transition-name: product-123-title">Product Name</h1>
</article>
Animation Patterns
For CSS animations including scroll-driven animations, transitions, and motion accessibility (prefers-reduced-motion), see the animation-motion skill.
CSS-Only Show/Hide Patterns
Content Reveal on Focus
/* Hidden by default */
[data-tooltip] {
position: relative;
&::after {
content: attr(data-tooltip);
position: absolute;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s;
}
&:hover::after,
&:focus::after {
opacity: 1;
visibility: visible;
}
}
Disclosure Widget
/* Summary arrow rotation */
details summary::before {
content: "▶";
display: inline-block;
transition: transform 0.2s;
margin-right: 0.5em;
}
details[open] summary::before {
transform: rotate(90deg);
}
Accessibility Considerations
Focus Management
CSS can style focus, but focus order requires HTML structure or JS:
/* Visible focus indicators */
:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* Remove default outline only when using custom */
:focus:not(:focus-visible) {
outline: none;
}
Reduced Motion
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Hidden Checkbox Labels
When using checkbox hacks, ensure labels are accessible:
<input type="checkbox" id="toggle" hidden/>
<label for="toggle">
<span class="sr-only">Toggle menu</span>
<span aria-hidden="true">☰</span>
</label>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
When JavaScript IS Needed
Progressive enhancement means JavaScript enhances, not enables. Use JS when:
- Complex state management - More than simple on/off
- Async operations - Fetching data, form submission
- Focus management - Moving focus programmatically
- Real-time updates - WebSocket, polling
- Complex animations - Scroll-triggered, gesture-based
Even then, provide a baseline:
- Forms should submit without JS
- Navigation should work without JS
- Content should be readable without JS
Graceful Degradation for JS-Required Applications
When an application requires JavaScript to function (SPAs, complex editors, real-time apps), provide graceful degradation patterns.
The <noscript> Element
Error Message in Body
Display a clear message when JavaScript is disabled:
<body>
<noscript>
<div class="js-required-notice">
<h2>JavaScript Required</h2>
<p>This application requires JavaScript to function.
Please enable JavaScript in your browser settings.</p>
</div>
</noscript>
<!-- App content -->
</body>
Hide JS-Only UI in Head
Use <noscript> in <head> to inject styles hiding JS-dependent elements:
<head>
<noscript>
<style>
[data-js-required] { display: none !important; }
.js-required-notice { display: block !important; }
</style>
</noscript>
</head>
<body>
<div class="js-required-notice" hidden>JavaScript is required.</div>
<main data-js-required>
<!-- JS-only app content -->
</main>
</body>
Redirect with Fallback Link
For full-page JS apps, redirect to a static fallback:
<head>
<noscript>
<meta http-equiv="refresh" content="0; url=/no-javascript.html"/>
</noscript>
</head>
<body>
<noscript>
<p>Redirecting... <a href="/no-javascript.html">Click here</a> if not redirected.</p>
</noscript>
</body>
CSS Scripting Media Query
Modern CSS can detect JavaScript availability:
/* When JS is disabled */
@media (scripting: none) {
[data-js-required] {
display: none;
}
.no-js-message {
display: block;
}
}
/* When JS is enabled */
@media (scripting: enabled) {
.no-js-message {
display: none;
}
}
Browser support: Chrome 120+, Firefox 113+, Safari 17+
Server-Side JS Detection
Track JavaScript-disabled users for analytics:
<!-- Image beacon in noscript -->
<noscript>
<img src="/api/analytics?js=disabled" alt="" width="1" height="1"/>
</noscript>
Or check for a JS-set cookie on the server:
// In your app initialization
document.cookie = 'js_enabled=true; path=/';
When to Use These Patterns
| Scenario | Recommended Pattern |
|----------|---------------------|
| Content-first pages | Don't need <noscript> - progressive enhancement handles it |
| SPA/complex web apps | <noscript> message + CSS hiding |
| Full-page JS apps | Redirect to static fallback |
| Analytics needs | Image beacon |
CSS Feature Detection with @supports
Use @supports for non-Baseline CSS features. This applies the same progressive enhancement philosophy to CSS itself.
Pattern: Baseline Fallback + Enhancement
/* Base: Baseline-safe fallback */
p {
word-break: break-word;
}
/* Enhancement: non-Baseline feature */
@supports (text-wrap: pretty) {
p {
text-wrap: pretty;
}
}
Common Progressive CSS Enhancements
/* Typography enhancement */
@supports (text-wrap: balance) {
h1, h2, h3 {
text-wrap: balance;
}
}
/* Color enhancement */
@supports (color: oklch(50% 0.2 260)) {
:root {
--primary: oklch(55% 0.22 260);
}
}
/* Container query fallback */
@supports not (container-type: inline-size) {
/* Fallback layout when container queries unavailable */
product-card {
display: block;
}
}
Multiple Feature Detection
/* Combine features with and/or */
@supports (display: grid) and (container-type: inline-size) {
product-grid {
display: grid;
container-type: inline-size;
}
}
/* Negation for fallbacks */
@supports not (gap: 1rem) {
.flex-container > * + * {
margin-inline-start: 1rem;
}
}
JavaScript Feature Detection
For features that can't be detected with @supports, use JavaScript with data attributes:
// Detect and expose to CSS
if (CSS.supports('view-transition-name', 'test')) {
document.documentElement.dataset.viewTransitions = 'supported';
}
[data-view-transitions="supported"] {
/* Enhanced styles */
}
Checklist
When building interactive features:
- [ ] Does it work without JavaScript?
- [ ] Is there semantic HTML structure?
- [ ] Are native elements used where applicable (
<details>,<dialog>,[popover])? - [ ] Are
command/commandforused for dialogs and popovers (notonclick)? - [ ] Is state managed via checkboxes/radios for CSS access?
- [ ] Are data attributes used instead of classes for state?
- [ ] Is
:has()used appropriately for parent styling? - [ ] Are
:user-valid/:user-invalidused over:valid/:invalid? - [ ] Is
prefers-reduced-motionrespected? - [ ] Are focus states visible and clear?
- [ ] Is View Transitions meta tag included?
- [ ] If JS is required, does
<noscript>provide a fallback message? - [ ] Are non-Baseline CSS features wrapped in
@supports?
Related Skills
- css-author - Modern CSS organization with native @import, @layer casca...
- javascript-author - Write vanilla JavaScript for Web Components with function...
- data-attributes - Using data-* attributes as the HTML/CSS/JS bridge for sta...
- forms - HTML-first form patterns with CSS-only validation
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/progressive-enhancement-html-first-development-mtz9jqth/download' --output '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md.part' && mv '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md.part' '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
Tools declared by the author (check availability in your agent): Read, Write, Edit
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.
Claude Code
test -f '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md' && test -s '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/progressive-enhancement' && cp -R '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth' '.claude/skills/progressive-enhancement'Cursor
test -f '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md' && test -s '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/progressive-enhancement' && cp -R '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth' '.cursor/skills/progressive-enhancement'Codex
test -f '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md' && test -s '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md' && mkdir -p '.agents/skills' && test ! -e '.agents/skills/progressive-enhancement' && cp -R '.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth' '.agents/skills/progressive-enhancement'OpenSkills
OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.
Official OpenSkills installationopenskills install './.skill-downloads/progressive-enhancement-html-first-development-mtz9jqth'openskills read 'progressive-enhancement-html-first-development-mtz9jqth'Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.
For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.
openskills syncManual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/progressive-enhancement-html-first-development-mtz9jqth/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.