You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
How is each interaction built with native CSS and Web APIs only? #10
Rules 1–6. The canvas has these behaviours: sticky blurred header, desktop nav versus mobile hamburger menu at 940px, FAQ accordion with one item open, smooth scroll to anchors, hover states, form submit to an inline success state, focus outlines. Decide for each:
The mechanism: CSS-only (:hover, scroll-behavior, container/media queries), native elements (<details>, Popover API for the menu, <dialog>), or minimal JS state.
The animation, its duration token, and the prefers-reduced-motion fallback.
How it avoids layout shift and never blocks interaction (e.g. accordion height animation via grid-template-rows or interpolate-size, menu as overlay).
Progressive enhancement: what still works with JS disabled on a prerendered page.
Prototype where behaviour is the question. Produce the interaction table as the resolution.
Progressive enhancement. Content, navigation, and the accordion work with JS disabled — they're pure HTML. The form is JS-only: Turnstile is a JS widget that injects cf-turnstile-response, and the server route validates exactly that field, so a tokenless submit would be rejected anyway. A tokenless fallback lane was rejected because bots are precisely the clients that don't run JS — it would become the only lane they use. A <noscript> line inside the form points at the phone number so the no-JS path is a stated alternative, not a dead button.
No /thanks page. Success is an inline swap to the canvas's existing role="status" panel, replacing the form. Chosen over a toast: it prevents a submitted-but-still-editable double-submit, role="status" announces politely without stealing focus, and a viewport-anchored toast can land off-screen on mobile given the Assessment section sits near the page bottom. Consequence for #14: no extra route to noindex or exclude from the sitemap.
The submit button is enabled from first paint and Turnstile runs on submit rather than gating the button — a slow widget must never leave the CTA dead (rule 5).
FAQ items lose their <h3>. MDN warns that some browsers give <summary> a button role that strips its children's roles, so a heading inside it is unreliable. The section keeps its <h2>; the six questions are navigated as buttons. The canvas's role="region" on the panel is also dropped — <details> permits no role, and the APG disclosure pattern doesn't ask for one. Trade accepted: FAQ questions no longer appear in a screen reader's heading outline.
Both CSS-only hacks rejected.:target exposes a link with no aria-expanded and writes a history entry per toggle (Back "closes" the menu) while fighting the sticky header via fragment scroll. The checkbox hack announces "checkbox, checked" for a disclosure. Their only historical argument was support, and <details> has been widely available since January 2020.
Popover is non-modal — no focus trap. Tab walks out of the open menu into the page behind. Accepted: making it modal means <dialog>.showModal() and JS, a poor trade for a six-link nav. Light dismiss, Esc, top-layer, and aria-expanded on the invoker all come free.
Support and degradation
Verified in Chromium via the prototype; the readout panel reports per-browser support.
Feature
Baseline
Degradation
<details name>
Newly, 2024-09
Panels open independently — accordion still works, just not exclusive
::details-content + discrete content-visibility
Newly, 2025-09
Firefox: instant open. No discrete content-visibility there
Popover API
Newly, 2025-01
Panel renders inline
@starting-style
Newly, 2024-08
Menu appears instantly
overlayallow-discrete
Limited — Chrome only
Menu snaps shut instead of fading
interpolate-size
Limited — Chrome only
Height jumps; only content fades. Bonus, never load-bearing
:has(), @media (hover:hover), :focus-visible
Widely
n/a
Nothing Limited-availability is load-bearing. CSS anchor positioning is not used — a full-width panel needs no anchoring.
Outstanding check: whether Firefox's instant accordion open reads as broken or merely plain. If unacceptable, the fallback is to drop the reveal animation in every browser so all engines match — a one-block deletion.
Implementation notes
[hidden] needs !important. The prototype caught this live: form and .success each set an explicit display, which beats the UA stylesheet's [hidden]{display:none}, so the success panel rendered from first paint and the form stayed visible after submit. app.css must carry [hidden]{display:none!important} after any rule setting display.
Reduced motion must name the pseudo-element.*, *::before, *::after does not cover ::details-content. The global block from #8 needs it added:
StyleX cannot express details[open] .marker. No descendant or sibling combinators. The marker rotation needs stylex.when.ancestor('[open]'). Same applies to the scroll-lock rule and every ::details-content rule — these are global-CSS concerns and belong in app.css, not in a StyleX namespace.
Hover guard shape — base styles outside the query, enhancement inside; @media (hover:hover) alone, never combined with (pointer:fine) (that describes pointer accuracy, a separate axis, and would exclude hover-capable coarse pointers for no benefit).
Popover positioning — the UA stylesheet centres [popover] in the viewport, so the full-width panel resets inset:auto; margin:0; top:<header-height>; left:0; right:0; max-width:none; border:0. Being position:fixed in the top layer, it ignores ancestor overflow and position. The ::backdrop is pointer-events:none !important and transparent — it can be dimmed but never intercepts clicks.
Layout shift. Accordion expansion is user-initiated, so it falls outside CLS (shifts within 500ms of input are excluded). The one real shift risk is scroll-lock removing the scrollbar, which scrollbar-gutter:stable prevents.
Consequences for other tickets
Which base components exist, and what is each one's interface? #9 (base components): needs Disclosure (<details>/<summary>), MenuPanel (popover), Card, Button, FormField. The [hidden], reduced-motion, scroll-lock and ::details-content rules are global CSS, not component styles.
Branch prototype/interactions, file prototypes/interactions.prototype.html — single file, no build. Verified in Chromium: declarative popover open/light-dismiss, native exclusivity (opening one panel closed the other), marker rotation, and the inline success swap. Open it in Firefox and Safari to see the degradation directly; the readout panel bottom-right lists what that browser supports.
Resolved. Interaction table in the resolution comment; prototype on branch prototype/interactions. One outstanding check noted there: whether Firefox's instant accordion open is acceptable, with a documented one-block fallback if not.
Activity
Abuubkar commentedon Sep 5, 2026
Resolved by grilling plus a browser-verified prototype. Every interaction is native-first; the only JavaScript on the page is the form submit handler.
Interaction table
position:sticky+backdrop-filter:blur(10px)@media (max-width: 940px)—screen.navfrom #8popovertargettranslateY(-8px),motion.base,@starting-stylehtml:has([popover]:popover-open){overflow:hidden}+scrollbar-gutter:stable<details name="faq">— exclusivity is nativetranslateY(-4px)on::details-content,content-visibilityallow-discrete+→×viarotate:45degrotate,motion.basescroll-behavior:smooth+scroll-margin-top:68pxscroll-behavior:autotranslateY(-2px)+ shadow, inside@media (hover:hover)motion.fast:hover— unguarded,:activefires on touchmotion.fast:focus-visible, 2pxfocusRing(teal700), offset 2pxfetch→ inline swap torole="status"panelDecisions
Progressive enhancement. Content, navigation, and the accordion work with JS disabled — they're pure HTML. The form is JS-only: Turnstile is a JS widget that injects
cf-turnstile-response, and the server route validates exactly that field, so a tokenless submit would be rejected anyway. A tokenless fallback lane was rejected because bots are precisely the clients that don't run JS — it would become the only lane they use. A<noscript>line inside the form points at the phone number so the no-JS path is a stated alternative, not a dead button.No
/thankspage. Success is an inline swap to the canvas's existingrole="status"panel, replacing the form. Chosen over a toast: it prevents a submitted-but-still-editable double-submit,role="status"announces politely without stealing focus, and a viewport-anchored toast can land off-screen on mobile given the Assessment section sits near the page bottom. Consequence for #14: no extra route tonoindexor exclude from the sitemap.The submit button is enabled from first paint and Turnstile runs on submit rather than gating the button — a slow widget must never leave the CTA dead (rule 5).
FAQ items lose their
<h3>. MDN warns that some browsers give<summary>abuttonrole that strips its children's roles, so a heading inside it is unreliable. The section keeps its<h2>; the six questions are navigated as buttons. The canvas'srole="region"on the panel is also dropped —<details>permits no role, and the APG disclosure pattern doesn't ask for one. Trade accepted: FAQ questions no longer appear in a screen reader's heading outline.Both CSS-only hacks rejected.
:targetexposes a link with noaria-expandedand writes a history entry per toggle (Back "closes" the menu) while fighting the sticky header via fragment scroll. The checkbox hack announces "checkbox, checked" for a disclosure. Their only historical argument was support, and<details>has been widely available since January 2020.Popover is non-modal — no focus trap. Tab walks out of the open menu into the page behind. Accepted: making it modal means
<dialog>.showModal()and JS, a poor trade for a six-link nav. Light dismiss, Esc, top-layer, andaria-expandedon the invoker all come free.Support and degradation
Verified in Chromium via the prototype; the readout panel reports per-browser support.
<details name>::details-content+ discretecontent-visibilitycontent-visibilitythere@starting-styleoverlayallow-discreteinterpolate-size:has(),@media (hover:hover),:focus-visibleNothing Limited-availability is load-bearing. CSS anchor positioning is not used — a full-width panel needs no anchoring.
Outstanding check: whether Firefox's instant accordion open reads as broken or merely plain. If unacceptable, the fallback is to drop the reveal animation in every browser so all engines match — a one-block deletion.
Implementation notes
[hidden]needs!important. The prototype caught this live:formand.successeach set an explicitdisplay, which beats the UA stylesheet's[hidden]{display:none}, so the success panel rendered from first paint and the form stayed visible after submit.app.cssmust carry[hidden]{display:none!important}after any rule settingdisplay.Reduced motion must name the pseudo-element.
*, *::before, *::afterdoes not cover::details-content. The global block from #8 needs it added:StyleX cannot express
details[open] .marker. No descendant or sibling combinators. The marker rotation needsstylex.when.ancestor('[open]'). Same applies to the scroll-lock rule and every::details-contentrule — these are global-CSS concerns and belong inapp.css, not in a StyleX namespace.Hover guard shape — base styles outside the query, enhancement inside;
@media (hover:hover)alone, never combined with(pointer:fine)(that describes pointer accuracy, a separate axis, and would exclude hover-capable coarse pointers for no benefit).Popover positioning — the UA stylesheet centres
[popover]in the viewport, so the full-width panel resetsinset:auto; margin:0; top:<header-height>; left:0; right:0; max-width:none; border:0. Beingposition:fixedin the top layer, it ignores ancestoroverflowandposition. The::backdropispointer-events:none !importantand transparent — it can be dimmed but never intercepts clicks.Layout shift. Accordion expansion is user-initiated, so it falls outside CLS (shifts within 500ms of input are excluded). The one real shift risk is scroll-lock removing the scrollbar, which
scrollbar-gutter:stableprevents.Consequences for other tickets
Disclosure(<details>/<summary>),MenuPanel(popover),Card,Button,FormField. The[hidden], reduced-motion, scroll-lock and::details-contentrules are global CSS, not component styles.fetch, not a redirect. No/thanksroute. Turnstile executes on submit, not on load.<noscript>line, and the success panel copy are all user-facing strings.Prototype
Branch
prototype/interactions, fileprototypes/interactions.prototype.html— single file, no build. Verified in Chromium: declarative popover open/light-dismiss, native exclusivity (opening one panel closed the other), marker rotation, and the inline success swap. Open it in Firefox and Safari to see the degradation directly; the readout panel bottom-right lists what that browser supports.Abuubkar commentedon Sep 5, 2026
Resolved. Interaction table in the resolution comment; prototype on branch
prototype/interactions. One outstanding check noted there: whether Firefox's instant accordion open is acceptable, with a documented one-block fallback if not.Header, navigation and mobile menu
Fit, Results and the FAQ accordion
Footer contrast, accordion hover and jerk, nav hover and scroll-spy, …