Skip to content

How is each interaction built with native CSS and Web APIs only? #10

Closed

Description

@Abuubkar

Question

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.

Activity

  1. self-assigned this
    on Sep 5, 2026
  2. Abuubkar commented on Sep 5, 2026

    @Abuubkar
    OwnerAuthor

    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

    # Interaction Mechanism Animation Reduced motion Without JS
    1 Sticky header position:sticky + backdrop-filter:blur(10px) none n/a ✅ works
    2 Desktop nav ↔ hamburger @media (max-width: 940px) — screen.nav from #8 none n/a ✅ works
    3 Mobile menu Popover API, declarative popovertarget fade + translateY(-8px), motion.base, @starting-style global block ✅ works
    4 Menu scroll lock html:has([popover]:popover-open){overflow:hidden} + scrollbar-gutter:stable none n/a ✅ works
    5 FAQ accordion <details name="faq"> — exclusivity is native opacity + translateY(-4px) on ::details-content, content-visibility allow-discrete global block ✅ works
    6 Accordion marker + → × via rotate:45deg rotate, motion.base global block ✅ works
    7 Smooth scroll scroll-behavior:smooth + scroll-margin-top:68px n/a scroll-behavior:auto ✅ works
    8 Card lift translateY(-2px) + shadow, inside @media (hover:hover) motion.fast global block ✅ works
    9 Button/link colour :hover — unguarded, :active fires on touch motion.fast global block ✅ works
    10 Focus ring :focus-visible, 2px focusRing (teal700), offset 2px none n/a ✅ works
    11 Form submit fetch → inline swap to role="status" panel none n/a ❌ JS required

    Decisions

    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
    overlay allow-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:

    @media (prefers-reduced-motion: reduce){
      *,*::before,*::after,*::details-content{
        animation-duration:.01ms!important; animation-iteration-count:1!important;
        transition-duration:.01ms!important; scroll-behavior:auto!important;
      }
    }

    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

    Prototype

    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.

  3. Abuubkar commented on Sep 5, 2026

    @Abuubkar
    OwnerAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

wayfinder:grillingWayfinder ticket — HITL conversation

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions