add progressive enhancement guidelines to CLAUDE.md
All checks were successful
deploy / deploy (push) Successful in 59s

Document the CSS :has(:checked) pattern for forms with dynamic
sections, noscript fallbacks for JS-only elements, and the HEEx
<style> raw text gotcha.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-04 23:28:21 +00:00
parent db130a7155
commit e24f9aa4f3

View File

@ -177,6 +177,48 @@ socket |> assign(form: to_form(changeset))
- Avoid LiveComponents unless you have a specific need (isolated state, targeted updates)
- Never use deprecated `phx-update="append"` or `phx-update="prepend"`
## Progressive Enhancement
Build HTML+CSS that works without JS. LiveView layers on top.
**Principle:** If a state change is purely visual (which radio is selected, which panel is shown), handle it in CSS. Don't round-trip to the server for something the browser can do alone.
**Pattern for forms with dynamic sections:**
1. Render ALL possible states in the HTML (e.g. all adapter configs, all option panels)
2. Use CSS `:has(:checked)` to show the matching section and hide the rest
3. Namespace field names to avoid clashes (`email[brevo][api_key]`, not `email[api_key]`)
4. Give the form both `action` (no-JS POST) and `phx-submit` (LiveView)
5. Add a thin controller to handle the no-JS POST path
```css
/* Hide all sections by default, show the one matching the checked radio */
.config-section[data-option] { display: none; }
form:has(#option-foo:checked) [data-option="foo"] { display: block; }
```
**JS-only elements:** Wrap in a container with an ID, hide it via `<noscript><style>`, and provide a form POST alternative:
```heex
<span id="async-action">
<.button type="button" phx-click="do_thing">Do thing</.button>
</span>
<noscript>
<style>#async-action { display: none; }</style>
<.form for={%{}} action={~p"/thing"} method="post" style="display:inline">
<.button>Do thing</.button>
</.form>
</noscript>
```
**HEEx and `<style>` tags:** HEEx treats `<style>` content as raw text (no `{@var}` interpolation). To output dynamic CSS, use a helper that returns `Phoenix.HTML.raw/1`:
```elixir
defp dynamic_style(value) do
Phoenix.HTML.raw("<style>.thing[data-x=\"#{value}\"] { display: block; }</style>")
end
```
## JS/CSS Guidelines
- Project is fully Tailwind-free — hand-written CSS with `@layer`, native nesting, `oklch()`