← Blurr Motion faq-popover-anchor-brutalist
Categorie faq Tier 3 Techniek #41 Deps gsap

Motion Lab / FAQ / popover anchor / brutalist

FREQUENTLY
ASKED.

  • Raw, ongepolijst, hard contrast. Geen rounded corners, geen gradients, alleen de essentie.
  • CSS-native plaatsing van popovers t.o.v. een trigger zonder JS-coordinaten. Stabiel bij scroll en resize.
  • Modern Chromium en Safari 17+. Voor oudere browsers vangen we het op met een classic positioned fallback.
  • Geen intro-tween, geen arrow-rotatie-animatie. De popover toggled direct, zonder beweging.
  • Ja. Native [popover] (auto) sluit de vorige automatisch zodra je een nieuwe opent. Brutalist, zoals het hoort.
1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// Mechanisme: faq-popover-anchor-brutalist (techniek #41 — Anchor positioning + Popover API)
// Native [popover] attribute + CSS anchor-name / position-anchor voor positioning naast de trigger.
// Feature-detect: 'popover' in HTMLElement.prototype && CSS.supports('anchor-name: --x').
// Fallback: klassiek absolute positioning t.o.v. parent. GSAP doet enkel snappy intro-reveal van items.
import gsap from 'https://esm.sh/gsap@3.12.5';
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const root = document.querySelector('.faq-pa-brut');
if (root) {
  const popoverSupport = 'popover' in HTMLElement.prototype;
  const anchorSupport = CSS.supports && CSS.supports('anchor-name: --x');
  if (!popoverSupport || !anchorSupport) root.classList.add('no-anchor');
  // Manual toggle (works for native popover + fallback)
  root.querySelectorAll('[data-pop-trigger]').forEach((btn) => {
    btn.addEventListener('click', (e) => {
      const id = btn.getAttribute('data-pop-target');
      const target = document.getElementById(id);
      if (!target) return;
      if (popoverSupport && target.togglePopover) {
        target.togglePopover();
      } else {
        target.classList.toggle('is-open');
      }
      // Toggle active accent on arrow
      root.querySelectorAll('[data-pop-trigger].is-active').forEach((b) => { if (b !== btn) b.classList.remove('is-active'); });
      btn.classList.toggle('is-active');
    });
  });
  if (!reduce) {
    gsap.from(root.querySelectorAll('[data-faq-item]'), {
      yPercent: 30, autoAlpha: 0, duration: 0.55, ease: 'power4.out', stagger: 0.03,
      scrollTrigger: { trigger: root, start: 'top 85%', once: true }
    });
  }
}
2. Skeleton — DOM + class-namen, mag herschikken
<!-- Skeleton: faq-popover-anchor-brutalist -->
<section class="faq-pa-brut">
  <ul class="faq-list">
    <li data-faq-item>
      <button data-pop-trigger data-pop-target="pop-0" popovertarget="pop-0">
        <span class="q">VRAAG?</span><span class="arrow">→</span>
      </button>
      <div id="pop-0" popover class="faq-pop">ANTWOORD</div>
    </li>
    <!-- 5 items totaal -->
  </ul>
</section>
3. Styling-template — verplicht eigen invulling per merk
/* Styling: faq-popover-anchor-brutalist */
:root {
  --block-bg: #F4F1EB;
  --block-fg: #0A0A0A;
  --block-accent: #FF4A1C;
}
.faq-pa-brut { background: var(--block-bg); color: var(--block-fg); padding: clamp(3rem,8vw,8rem) clamp(1.5rem,4vw,4rem); font-family: 'Archivo', sans-serif; }
.faq-pa-brut .faq-list { list-style: none; margin: 0; padding: 0; max-width: 880px; }
.faq-pa-brut [data-faq-item] { border-top: 1px solid var(--block-fg); }
.faq-pa-brut [data-faq-item]:last-child { border-bottom: 1px solid var(--block-fg); }
.faq-pa-brut [data-pop-trigger] {
  width: 100%; background: transparent; border: 0; padding: 1.5rem 0; cursor: pointer;
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  font-family: 'Archivo', sans-serif; font-weight: 700; font-size: clamp(1.25rem, 3vw, 2rem);
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--block-fg); text-align: left;
}
.faq-pa-brut [data-pop-trigger] .arrow { transition: color .12s linear, transform .12s linear; }
.faq-pa-brut [data-pop-trigger].is-active .arrow { color: var(--block-accent); transform: rotate(45deg); }
/* Anchor positioning per item */
.faq-pa-brut .pop-anchor-0 { anchor-name: --pop-0; }
.faq-pa-brut .pop-anchor-1 { anchor-name: --pop-1; }
.faq-pa-brut .pop-anchor-2 { anchor-name: --pop-2; }
.faq-pa-brut .pop-anchor-3 { anchor-name: --pop-3; }
.faq-pa-brut .pop-anchor-4 { anchor-name: --pop-4; }
.faq-pa-brut .faq-pop {
  margin: 0; padding: 1.5rem; max-width: 420px;
  background: var(--block-fg); color: var(--block-bg);
  border: 1px solid var(--block-fg); border-radius: 0;
  font-family: 'Archivo', sans-serif; font-weight: 500; font-size: 1rem; line-height: 1.4;
}
.faq-pa-brut #pop-0 { position-anchor: --pop-0; }
.faq-pa-brut #pop-1 { position-anchor: --pop-1; }
.faq-pa-brut #pop-2 { position-anchor: --pop-2; }
.faq-pa-brut #pop-3 { position-anchor: --pop-3; }
.faq-pa-brut #pop-4 { position-anchor: --pop-4; }
.faq-pa-brut .faq-pop { top: anchor(bottom); left: anchor(right); margin-left: 1rem; }
.faq-pa-brut .faq-pop::part(arrow), .faq-pa-brut .faq-pop .pop-arrow {
  position: absolute; top: 1.25rem; left: -1rem; width: 0; height: 0;
  border-top: 8px solid transparent; border-bottom: 8px solid transparent;
  border-right: 12px solid var(--block-accent);
}
/* Fallback no-anchor: stack popover under trigger */
.faq-pa-brut.no-anchor .faq-pop { position: static; display: none; max-width: none; margin: 0 0 1rem 0; }
.faq-pa-brut.no-anchor .faq-pop.is-open { display: block; }
@media (prefers-reduced-motion: reduce) {
  .faq-pa-brut [data-pop-trigger] .arrow { transition: none; }
}