interactions · flip · scroll

Cartes qui se retournent au scroll

4 cartes en 3D qui se retournent une par une pendant que tu scrolles (pas au survol, pas en boucle automatique). Contenu provisoire, à remplacer selon ta charte finale.

Effets

Voir le code source
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Inter:wght@400;500;600&display=swap');

.bn-flip-cards {
  padding: 3.5rem 2rem;
  text-align: center;
  font-family: 'Inter', sans-serif;
  background: #050808;
}

.bn-flip-cards__kicker {
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #979793;
  margin: 0 0 0.5rem;
}

.bn-flip-cards__title {
  font-family: 'Bebas Neue', sans-serif;
  font-size: 1.9rem;
  letter-spacing: 0.02em;
  color: #E6E4DD;
  margin: 0 0 2.5rem;
}

.bn-flip-cards__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.25rem;

  /* "perspective" donne la profondeur en 3D à toutes les cartes
     enfants — sans elle, la rotation aurait l'air plate, sans volume */
  perspective: 1200px;
}

.bn-flip-cards__card {
  width: 170px;
  height: 210px;
}

.bn-flip-cards__inner {
  position: relative;
  width: 100%;
  height: 100%;

  /* "preserve-3d" est ce qui permet aux deux faces (avant/arrière)
     de rester positionnées en 3D l'une par rapport à l'autre au
     lieu d'être aplaties */
  transform-style: preserve-3d;
  transition: transform 0.1s linear;
}

.bn-flip-cards__face {
  position: absolute;
  inset: 0;

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 1.5rem;

  background: #0B0F10;
  border: 1px solid rgba(230, 228, 221, 0.12);
  border-radius: 8px;

  /* Cache la face qui est actuellement retournée "dos à l'écran" —
     sans ça, on verrait le texte de la face arrière à l'envers en
     transparence par-dessus la face avant */
  backface-visibility: hidden;
}

.bn-flip-cards__face--back {
  /* Pré-tournée à 180° : elle ne devient visible à l'endroit que
     quand la carte entière a fini de pivoter */
  transform: rotateY(180deg);
}

.bn-flip-cards__icon {
  font-size: 1.6rem;
  color: #979793;
}

.bn-flip-cards__label {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #979793;
}

.bn-flip-cards__face--back p {
  margin: 0;
  color: #E6E4DD;
  font-size: 0.95rem;
  line-height: 1.5;
}

.bn-flip-cards__hint {
  margin: 2.5rem 0 0;
  color: #5C6160;
  font-size: 0.8rem;
}

/* Effet désactivé : toutes les cartes restent face avant, immobiles */
.bn-flip-cards:not(.fx-flip-scroll) .bn-flip-cards__inner {
  transform: rotateY(0deg) !important;
}

</style>

<div class="bn-flip-cards fx-flip-scroll" data-fx-root>
  <p class="bn-flip-cards__kicker">02 // Quatre mouvements</p>
  <h2 class="bn-flip-cards__title">Quatre mouvements.</h2>

  <!-- data-flip-row : sert de repère au script pour calculer la
       progression du scroll. Chaque carte a une face avant (icône +
       nom) et une face arrière (une phrase), superposées en 3D. -->
  <div class="bn-flip-cards__row" data-flip-row>

    <div class="bn-flip-cards__card">
      <div class="bn-flip-cards__inner">
        <div class="bn-flip-cards__face bn-flip-cards__face--front">
          <span class="bn-flip-cards__icon">◎</span>
          <span class="bn-flip-cards__label">01 — Observer</span>
        </div>
        <div class="bn-flip-cards__face bn-flip-cards__face--back">
          <p>Voir ce qui est là. Et parfois ce que personne ne regarde.</p>
        </div>
      </div>
    </div>

    <div class="bn-flip-cards__card">
      <div class="bn-flip-cards__inner">
        <div class="bn-flip-cards__face bn-flip-cards__face--front">
          <span class="bn-flip-cards__icon">◈</span>
          <span class="bn-flip-cards__label">02 — Questionner</span>
        </div>
        <div class="bn-flip-cards__face bn-flip-cards__face--back">
          <p>Refuser les réponses simplement parce qu'elles existent déjà.</p>
        </div>
      </div>
    </div>

    <div class="bn-flip-cards__card">
      <div class="bn-flip-cards__inner">
        <div class="bn-flip-cards__face bn-flip-cards__face--front">
          <span class="bn-flip-cards__icon">✳</span>
          <span class="bn-flip-cards__label">03 — Construire</span>
        </div>
        <div class="bn-flip-cards__face bn-flip-cards__face--back">
          <p>Donner une forme aux idées pour voir si elles tiennent debout.</p>
        </div>
      </div>
    </div>

    <div class="bn-flip-cards__card">
      <div class="bn-flip-cards__inner">
        <div class="bn-flip-cards__face bn-flip-cards__face--front">
          <span class="bn-flip-cards__icon">◑</span>
          <span class="bn-flip-cards__label">04 — Épurer</span>
        </div>
        <div class="bn-flip-cards__face bn-flip-cards__face--back">
          <p>Retirer le bruit jusqu'à retrouver le signal.</p>
        </div>
      </div>
    </div>

  </div>

  <p class="bn-flip-cards__hint">
    ↕ Scrollez cette page pour voir chaque carte se retourner, l'une après l'autre.
  </p>
</div>


<script>
/* ==========================================================
   CARTES QUI SE RETOURNENT AU SCROLL
   Chaque carte a une plage de progression qui lui est propre
   (la carte 1 tourne entre 0% et 25% du scroll de la rangée, la
   carte 2 entre 25% et 50%, etc.) — c'est ce qui crée l'effet de
   retournement SÉQUENTIEL plutôt que toutes en même temps.

   Même principe que le parallax et le fondu au scroll : on écoute
   le scroll de LA VRAIE PAGE (window.parent si affiché dans une
   iframe, sinon window directement).
   ========================================================== */

(function () {
  const root = document.querySelector('.bn-flip-cards[data-fx-root]');
  if (!root) return;

  const row = root.querySelector('[data-flip-row]');
  const cards = root.querySelectorAll('.bn-flip-cards__card');

  const isInsideFrame = Boolean(window.frameElement);
  const scrollTarget = isInsideFrame ? window.parent : window;

  function frameOffsetTop() {
    return isInsideFrame ? window.frameElement.getBoundingClientRect().top : 0;
  }

  function viewportHeight() {
    return isInsideFrame ? window.parent.innerHeight : window.innerHeight;
  }

  function update() {
    if (!root.classList.contains('fx-flip-scroll')) {
      cards.forEach((card) => {
        card.querySelector('.bn-flip-cards__inner').style.transform = 'rotateY(0deg)';
      });
      return;
    }

    const rowTopInPage = frameOffsetTop() + row.getBoundingClientRect().top;

    // Progression globale de 0 (la rangée est encore en bas de
    // l'écran) à 1 (elle est arrivée en haut de l'écran)
    let progress = (viewportHeight() - rowTopInPage) / viewportHeight();
    progress = Math.max(0, Math.min(1, progress));

    cards.forEach((card, index) => {
      const start = index / cards.length;
      const end = (index + 1) / cards.length;

      // La progression LOCALE de cette carte précise, ramenée entre 0 et 1
      let local = (progress - start) / (end - start);
      local = Math.max(0, Math.min(1, local));

      const angle = local * 180;
      card.querySelector('.bn-flip-cards__inner').style.transform = `rotateY(${angle}deg)`;
    });
  }

  scrollTarget.addEventListener('scroll', update, { passive: true });
  window.addEventListener('resize', update);
  update();
})();

</script>