CartesGratuit

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.

CSSclip-pathToggle
Survolez ou cliquez la scène pour interagir

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

Votre panier2 articles
Chemise en lin · M69 €
Ceinture cuir45 €
Un code pour vous
Total114 €

Survolez le coin, puis cliquez

① Code promo — panneau de panier ou pop-in e-commerce

QuandColonne de panier : deux articles, un total, et un bloc « un code pour vous » à découvrir.
PourquoiLa récompense reste cachée jusqu'à un geste volontaire : le coin corné invite, le clic engage — un seul bouton natif, pas de bibliothèque. Le format 250 × 160 tient dans une colonne latérale et le code reste dans le DOM pour être copié.
RéglagesCode vendu, carte réduite à 250 × 140 px, valeur de clic portée à 420px (≥ largeur + hauteur) pour que la page disparaisse entièrement, font: inherit; cursor: pointer sur le bouton.
Jour 7 · Série de fidélité Votre récompense du jour Revenez demain pour le jour 8.

Survolez le coin, puis cliquez

② Récompense du jour — programme de fidélité

QuandÉcran quotidien d'une application de fidélité : un compteur de série, une carte à corner, le rendez-vous du lendemain.
PourquoiLe geste de la page qui se corne se lit comme une carte à gratter, sans canvas ni pointer events : la couche du dessous est du HTML libre (emoji, montant, validité). L'état reste ouvert après le clic, le temps de lire.
RéglagesCarte 280 × 170 px → clic à 460px, repos 40px, survol 170px, transitions .7s (papier plus lourd), halo et textes passés en ambre (#ffd166 = 13:1 sur #17120a).
GéographieCarte 12 sur 30
← PrécédenteJe savais ✓Suivante →

Survolez le coin, puis cliquez

③ Carte de révision — question au recto, réponse dessous

QuandApplication d'apprentissage : la question sur la page, la réponse et son explication en dessous, des commandes précédente / suivante.
PourquoiUn bouton à bascule est exactement ce qu'il faut pour révéler puis recacher une réponse ; le coin corné signale qu'il y a quelque chose sous la carte sans le dire. Rien ne se referme tout seul : l'apprenant décide.
RéglagesCarte 300 × 180 px → clic à 490px, survol 160px, texte de la page 14 px sur deux lignes (padding: 0 28px; line-height: 1.4), halo bleu, sous-ligne de la réponse en minuscules.

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 button natif dont aria-pressed bascule — annoncé comme bouton à bascule, activable à Entrée et Espace. Mais l'aria-label remplace 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" quand aria-pressed passe à true), et traduisez-le : le HTML vendu est en français. Les trois div à l'intérieur du button sont invalides (un bouton n'accepte que du contenu phrasé) : remplacez-les par des span en display: block ou grid.
  • Contraste mesuré (Chromium, pixels sous le texte) : texte de la page #cfcfe8 12,8 px sur le dégradé #262640 → #16162a = 9,6 à 11,6:1 (AAA) ; code #22ffaa 20 px sur #0d1512 = 14:1, 9:1 au cœur du halo ; sous-ligne #7fbfa8 10,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-visible dessine outline: 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 button ne reçoit pas la police de la page : sans font: 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 reste default sur une carte cliquable : ajoutez font: 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.

Chrome 87+✓ Complet
Firefox 66+✓ Complet
Safari 14.1+✓ Complet
Edge 87+✓ Complet
Mobile iOS 14.5+✓ Complet (le tap révèle, pas d'aperçu)
Android Chrome✓ Complet (le tap révèle, pas d'aperçu)

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.

HTML
<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>
CSS
.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
  }
}
JavaScript (fx-0775)
(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éfautEffet
--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.