--- 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 ### `
` and `` for Accordions No JavaScript needed for expand/collapse: ```html
What is your return policy?

We offer a 30-day hassle-free return policy for all unused items.

``` ```css 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); } } ``` ### `` for Modals Native modal with backdrop and focus trapping—**fully declarative with `command`/`commandfor`**: ```html

Settings

``` ```css 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-expanded` states - Backdrop clicks (with `method="dialog"`) ### Popover API Popovers provide lightweight, non-modal overlays: ```html ``` ```css [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 | `` | `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: ```html ``` #### Built-in Commands | Command | Target | Effect | |---------|--------|--------| | `show-modal` | `` | Opens as modal | | `close` | `` | Closes dialog | | `toggle-popover` | `[popover]` | Toggles popover | | `show-popover` | `[popover]` | Opens popover | | `hide-popover` | `[popover]` | Closes popover | #### Custom Commands Define custom behaviors with `--` prefix: ```html ``` ```javascript // 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 | ### `` for Autocomplete ```html ``` --- ## CSS-Only Interactivity Patterns ### The Checkbox Hack Use hidden checkboxes to toggle state without JavaScript: ```html ``` ```css 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 ```html
Theme
``` ```css :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 ```html
Overview content...
Features content...
Pricing content...
``` ```css [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 ```css /* 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 ```css /* 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: ```css /* 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: ```css /* 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: ```css input:placeholder-shown { /* Empty input styles */ } input:not(:placeholder-shown) { /* Has content */ } ``` ### `:required` and `:optional` ```css 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 ```html ``` ### Default Transition With the meta tag, all same-origin navigations get a crossfade by default. ### Named Transitions ```css /* 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 ```html

Product Name

Product Name

``` --- ## 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 ```css /* 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 ```css /* 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: ```css /* 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 ```css @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: ```html ``` ```css .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: 1. **Complex state management** - More than simple on/off 2. **Async operations** - Fetching data, form submission 3. **Focus management** - Moving focus programmatically 4. **Real-time updates** - WebSocket, polling 5. **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 `