Survolez le coin, puis cliquez
Page Curl Reveal
Une page sombre recouvre un code promo : au survol son coin haut-droit se corne de 34 à 150 px (clip-path polygonal + pli dégradé, 550 ms), au clic elle se retire et le contenu apparaît — un bouton natif à aria-pressed, six lignes de JS. Mesuré : la valeur de clic livrée (380 px) laisse un triangle de 26 px en bas à gauche.
Cet effet est gratuit — la catégorie Cartes en contient 41 au total, dont 6 gratuits. Effect.Labs regroupe 811 effets vanilla. Découvrir la catégorie →
Exemples d'usage
Survolez le coin, puis cliquez
Survolez le coin, puis cliquez
Comment ça marche
.pcr-card est un button de 250 × 160 px (position: relative; overflow: hidden; border-radius: 12px, fond #101018, bordure blanche à 8 %) qui empile trois calques en position: absolute; inset: 0 : .pcr-under, la récompense (grille centrée, halo radial-gradient vert rgba(34,255,170,.18) à 70 % 20 % sur #0d1512, code en 1,25 rem #22ffaa, sous-ligne .68 rem #7fbfa8 en capitales), .pcr-page par-dessus (dégradé 135° #262640 → #16162a, texte .8 rem #cfcfe8), et .pcr-curl, le pli, calé en haut à droite et insensible au pointeur. Une seule longueur commande tout : --pcr-c, la taille du coin retiré.
La découpe : .pcr-page porte clip-path: polygon(0 0, calc(100% − c) 0, 100% c, 100% 100%, 0 100%) — un pentagone dont le sommet haut-droit est remplacé par une diagonale à 45° qui ôte un triangle rectangle de côté c. Au repos c = 34px ; au :hover et au :focus-visible, c = 150px. La variable n'est pas enregistrée par @property, elle saute donc d'une valeur à l'autre — mais la transition porte sur clip-path lui-même (.55s cubic-bezier(.25,.8,.3,1)) : le navigateur interpole entre les deux polygones calculés, qui ont le même nombre de sommets, et le pli glisse. Pas besoin de @property ici, contrairement aux effets qui animent la variable.
Le pli : .pcr-curl est un carré c × c (width et height en transition .55 s) peint par linear-gradient(225deg, transparent 48%, #3d3d66 50%, #52528a 70%, #2c2c4d 100%) — la moitié tournée vers le coin retiré est transparente, l'autre dessine le dos de la page avec un centre clair #52528a entre deux bords sombres ; border-bottom-left-radius: 55% arrondit la boucle du pli et box-shadow: -5px 5px 12px rgba(0,0,0,.45) la pose sur la page. Comme la découpe et le pli partagent c, l'hypoténuse du pli et le bord de la découpe coïncident à chaque instant.
Le clic : le script (six lignes) pose aria-pressed="false" sur chaque .pcr-card et l'inverse au clic. [aria-pressed="true"] porte c à 380 px sur la page (y compris sous :hover, pour que le survol ne la referme pas) et le pli à 380 × 380 px avec opacity: 0 (transition .6 s, opacité .35 s retardée de .3 s). Mesuré dans Chromium : à 380 px sur une carte de 250 × 160, la diagonale (x − y = −130) laisse encore un triangle rectangle de 26 px de côté en bas à gauche — 331 pixels de page restent visibles après le clic ; la page ne disparaît qu'à partir de c = largeur + hauteur, soit 410 px. Second clic : retour à 34 px. prefers-reduced-motion: reduce supprime les deux transitions.
Accessibilité
- prefers-reduced-motion géré :
.pcr-page, .pcr-curl { transition: none }— les états s'appliquent instantanément, la révélation reste complète. L'un des rares effets du catalogue livrés avec cette règle. - Sémantique : un
buttonnatif dontaria-pressedbascule — annoncé comme bouton à bascule, activable à Entrée et Espace. Mais l'aria-labelremplace le contenu : un lecteur d'écran n'entend jamais « WELCOME20 », la récompense lui est invisible. Mettez le code dans le libellé une fois révélé (aria-label="Code promo WELCOME20, −20 % sur tout"quandaria-pressedpasse à true), et traduisez-le : le HTML vendu est en français. Les troisdivà l'intérieur dubuttonsont invalides (un bouton n'accepte que du contenu phrasé) : remplacez-les par desspanendisplay: blockougrid. - Contraste mesuré (Chromium, pixels sous le texte) : texte de la page
#cfcfe812,8 px sur le dégradé#262640 → #16162a= 9,6 à 11,6:1 (AAA) ; code#22ffaa20 px sur#0d1512= 14:1, 9:1 au cœur du halo ; sous-ligne#7fbfa810,9 px = 8,75:1, 5,6:1 sur le halo — tout dépasse 4,5:1, aucune couleur à changer. Seule la taille de 10,9 px est petite pour une information utile. - Clavier : Tab atteint le bouton,
:focus-visibledessineoutline: 2px solid #a78bfaà 3 px et corne le coin à 150 px — le même aperçu qu'au survol ; Entrée ou Espace révèle. Tactile : pas de survol, un tap révèle directement (l'aperçu à 150 px n'existe pas au doigt) ; cible 250 × 160 px. - Le
buttonne reçoit pas la police de la page : sansfont: inherit, le texte s'affiche dans la police par défaut des boutons (Arial 13,33 px dans Chromium) — les tailles en rem suivent la racine, pas la famille. Et le curseur restedefaultsur une carte cliquable : ajoutezfont: inherit; cursor: pointer.
Compatibilité navigateur
Requiert clip-path: polygon() avec calc() et var(), la transition de clip-path, le raccourci inset et place-items — la propriété limitante est inset. JavaScript ES5, zéro dépendance.
Sans JavaScript, le survol et le focus cornent toujours le coin à 150 px, mais rien ne révèle le code : ajoutez un repli serveur ou affichez le code ailleurs. Sans transition de clip-path (navigateurs très anciens), les états sautent d'un coup ; sans inset, les trois calques n'ont ni taille ni position et seul le fond de la carte reste.
Le code
Copiez les trois blocs dans votre page. Aucune dépendance.
<button class="pcr-card" type="button" aria-label="Survoler ou activer pour corner la page et révéler le code promo">
<div class="pcr-under"><div><strong>WELCOME20</strong><span>-20 % sur tout</span></div></div>
<div class="pcr-page">Survolez le coin ↗</div>
<span class="pcr-curl" aria-hidden="true"></span>
</button>
.pcr-card {
position: relative;
width: 250px;
height: 160px;
border-radius: 12px;
overflow: hidden;
background: #101018;
border: 1px solid rgba(255,255,255,.08);
}
.pcr-under {
position: absolute;
inset: 0;
display: grid;
place-items: center;
text-align: center;
background: radial-gradient(circle at 70% 20%, rgba(34,255,170,.18), transparent 55%), #0d1512;
}
.pcr-under strong {
font-size: 1.25rem; color: #22ffaa; letter-spacing: .06em
}
.pcr-under span {
display: block; font-size: .68rem; color: #7fbfa8; margin-top: 4px; letter-spacing: .12em; text-transform: uppercase
}
.pcr-page {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: linear-gradient(135deg, #262640, #16162a);
color: #cfcfe8;
font-size: .8rem;
letter-spacing: .04em;
clip-path: polygon(0 0, calc(100% - var(--pcr-c)) 0, 100% var(--pcr-c), 100% 100%, 0 100%);
transition: clip-path .55s cubic-bezier(.25,.8,.3,1);
--pcr-c: 34px;
}
.pcr-curl {
position: absolute;
top: 0; right: 0;
width: var(--pcr-c);
height: var(--pcr-c);
--pcr-c: 34px;
background: linear-gradient(225deg, transparent 48%, #3d3d66 50%, #52528a 70%, #2c2c4d 100%);
border-bottom-left-radius: 55%;
box-shadow: -5px 5px 12px rgba(0,0,0,.45);
transition: width .55s cubic-bezier(.25,.8,.3,1), height .55s cubic-bezier(.25,.8,.3,1);
pointer-events: none;
}
.pcr-card:hover .pcr-page, .pcr-card:focus-visible .pcr-page {
--pcr-c: 150px
}
.pcr-card:hover .pcr-curl, .pcr-card:focus-visible .pcr-curl {
width: 150px; height: 150px
}
.pcr-card:focus-visible {
outline: 2px solid #a78bfa; outline-offset: 3px
}
.pcr-card[aria-pressed="true"] .pcr-page {
--pcr-c: 380px
}
.pcr-card[aria-pressed="true"] .pcr-curl {
width: 380px; height: 380px; opacity: 0; transition: width .6s cubic-bezier(.25,.8,.3,1), height .6s cubic-bezier(.25,.8,.3,1), opacity .35s ease .3s
}
.pcr-card[aria-pressed="true"]:hover .pcr-page {
--pcr-c: 380px
}
@media (prefers-reduced-motion: reduce) {
.pcr-page, .pcr-curl {
transition: none
}
}
(function() {
/* fx-0775 — Page Curl Reveal : clic = révélation complète (toggle), survol = pli */
document.querySelectorAll('.pcr-card').forEach(function(card) {
card.setAttribute('aria-pressed', 'false');
card.addEventListener('click', function() {
card.setAttribute('aria-pressed', card.getAttribute('aria-pressed') !== 'true');
});
});
})();
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
--pcr-c au repos (CSS — .pcr-page et .pcr-curl) |
34px | Taille du coin corné au repos. 0 = page plate sans invitation ; 48px = coin bien visible. Posez la même valeur sur les deux éléments : la variable est déclarée sur chacun, pas héritée entre frères. |
--pcr-c au survol (CSS — :hover / :focus-visible, + width/height du pli) |
150px | Ampleur de l'aperçu. Le pli est opaque et se rabat sur la page : à 150 px sur une carte de 250 px, il recouvre déjà la moitié droite du libellé centré (visible dans la démo). Descendez à 110 px pour un libellé entier. Trois valeurs à changer ensemble : la variable de la page, la largeur et la hauteur du pli. |
--pcr-c au clic (CSS — [aria-pressed="true"], deux règles) |
380px | Doit valoir au moins largeur + hauteur de la carte (410 px pour 250 × 160) pour que la page disparaisse ; à 380 px un triangle de 26 px subsiste en bas à gauche. Prévoyez 10 px de marge. |
| Durée et courbe (CSS — transition .55s cubic-bezier(.25,.8,.3,1)) | .55s | Courbe qui décélère : le pli part vite et se pose. .35s = sec ; .8s = papier lourd. Les trois transitions (clip-path, width, height) doivent rester synchrones, sinon le pli décolle de la découpe. |
| Dos de la page (CSS — .pcr-curl, linear-gradient 225deg) | transparent 48% → #3d3d66 → #52528a 70% → #2c2c4d | La matière du pli. Éclaircissez l'arrêt à 70 % pour un papier brillant ; gardez le transparent sous 50 % pour que le triangle vide reste aligné sur la découpe. |
| Dimensions (CSS — .pcr-card width / height) | 250 × 160 px | Format de la carte. Recalculez la valeur de clic (largeur + hauteur) et, si la carte est large, le survol. Les calques suivent via inset: 0. |
| Contenu caché (HTML — .pcr-under) | code + sous-ligne | HTML libre dans une grille centrée : texte, emoji, image. Tout ce qui s'y trouve est masqué à l'écran mais présent dans le DOM — lu par les lecteurs d'écran si l'aria-label ne l'écrase pas, copiable par Ctrl+A. |
FAQ
Après le clic, un petit coin de la page reste visible en bas à gauche — pourquoi ?
Par géométrie. La découpe retire tout ce qui est au-dessus de la diagonale x − y = largeur − c ; à c = 380px sur 250 × 160, la diagonale passe par (0, 130) et (30, 160) : le triangle en dessous, 26 px de côté, reste couvert par la page. Mesuré dans Chromium : 331 pixels de page après le clic. Il faut c ≥ largeur + hauteur (410 px) ; portez les deux règles [aria-pressed="true"] à 420px.
La variable --pcr-c n'est pas enregistrée par @property : comment le coin peut-il glisser ?
Parce que ce n'est pas la variable qui est animée mais clip-path, width et height, dont les valeurs calculées changent quand la variable change. Le navigateur interpole deux polygones qui ont le même nombre de sommets — c'est pour cela que le pentagone garde cinq points même au repos. @property ne serait nécessaire que pour animer --pcr-c elle-même.
Peut-on révéler au survol seul, sans clic — ou supprimer l'aperçu au survol ?
Pour un survol révélateur : portez les règles :hover à --pcr-c: 420px et le pli à 420 px, supprimez le JS ; la page revient dès que la souris sort. Pour supprimer l'aperçu (comportement tactile partout) : retirez les règles :hover et gardez celles de :focus-visible. Pour refermer automatiquement : un setTimeout qui repasse aria-pressed à false après quelques secondes.