interactions · flip · 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
<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>