CardsFree

Page Curl Reveal

A dark page covers a promo code: on hover its top-right corner curls from 34 to 150 px (polygon clip-path + gradient fold, 550 ms), on click it pulls away and the content appears — a native button with aria-pressed, six lines of JS. Measured: the shipped click value (380 px) leaves a 26 px triangle in the bottom-left corner.

CSSclip-pathToggle
Hover or click the scene to interact

This effect is free — the Cards category contains 41 effects total, including 6 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

Your cart2 items
Linen shirt · M€69
Leather belt€45
A code for you
Total€114

Hover the corner, then click

① Promo code — cart panel or e-commerce pop-in

WhenCart column: two items, a total, and an “a code for you” block to discover.
WhyThe reward stays hidden until a deliberate gesture: the curled corner invites, the click commits — a single native button, no library. The 250 × 160 format fits a side column and the code stays in the DOM to be copied.
SettingsCode as sold, card reduced to 250 × 140 px, click value raised to 420px (≥ width + height) so the page fully disappears, font: inherit; cursor: pointer on the button.
Day 7 · Loyalty streak Your reward of the day Come back tomorrow for day 8.

Hover the corner, then click

② Reward of the day — loyalty program

WhenDaily screen of a loyalty app: a streak counter, a card to curl, tomorrow's appointment.
WhyThe curling-page gesture reads like a scratch card, with no canvas or pointer events: the layer underneath is free HTML (emoji, amount, validity). The state stays open after the click, long enough to read.
Settings280 × 170 px card → click at 460px, rest 40px, hover 170px, .7s transitions (heavier paper), halo and texts switched to amber (#ffd166 = 13:1 on #17120a).
GeographyCard 12 of 30
← PreviousI knew it ✓Next →

Hover the corner, then click

③ Flashcard — question on the front, answer underneath

WhenLearning app: the question on the page, the answer and its explanation underneath, previous / next controls.
WhyA toggle button is exactly what's needed to reveal then re-hide an answer; the curled corner signals there's something under the card without saying so. Nothing closes on its own: the learner decides.
Settings300 × 180 px card → click at 490px, hover 160px, 14 px page text on two lines (padding: 0 28px; line-height: 1.4), blue halo, answer sub-line in lowercase.

How it works

.pcr-card is a 250 × 160 px button (position: relative; overflow: hidden; border-radius: 12px, #101018 background, white border at 8%) stacking three layers in position: absolute; inset: 0: .pcr-under, the reward (centered grid, green radial-gradient halo rgba(34,255,170,.18) at 70% 20% on #0d1512, code at 1.25 rem #22ffaa, .68 rem uppercase sub-line #7fbfa8), .pcr-page on top (135° gradient #262640 → #16162a, .8 rem text #cfcfe8), and .pcr-curl, the fold, anchored top-right and ignoring the pointer. One length drives everything: --pcr-c, the size of the removed corner.

The cut: .pcr-page carries clip-path: polygon(0 0, calc(100% − c) 0, 100% c, 100% 100%, 0 100%) — a pentagon whose top-right vertex is replaced by a 45° diagonal that removes a right triangle with legs c. At rest c = 34px; on :hover and :focus-visible, c = 150px. The variable isn't registered with @property, so it jumps from one value to the other — but the transition is on clip-path itself (.55s cubic-bezier(.25,.8,.3,1)): the browser interpolates between the two computed polygons, which have the same number of vertices, and the fold glides. No @property needed here, unlike effects that animate the variable.

The fold: .pcr-curl is a c × c square (width and height transitioned over .55 s) painted with linear-gradient(225deg, transparent 48%, #3d3d66 50%, #52528a 70%, #2c2c4d 100%) — the half facing the removed corner is transparent, the other half draws the back of the page with a light #52528a center between two darker edges; border-bottom-left-radius: 55% rounds the loop of the fold and box-shadow: -5px 5px 12px rgba(0,0,0,.45) sets it on the page. Because the cut and the fold share c, the fold's hypotenuse and the cut edge coincide at every instant.

The click: the script (six lines) sets aria-pressed="false" on every .pcr-card and flips it on click. [aria-pressed="true"] pushes c to 380 px on the page (also under :hover, so hovering can't close it) and the fold to 380 × 380 px with opacity: 0 (.6 s transition, opacity .35 s delayed by .3 s). Measured in Chromium: at 380 px on a 250 × 160 card the diagonal (x − y = −130) still leaves a right triangle with 26 px legs in the bottom-left — 331 page pixels remain visible after the click; the page only disappears from c = width + height, i.e. 410 px. Second click: back to 34 px. prefers-reduced-motion: reduce removes both transitions.

Accessibility

  • prefers-reduced-motion handled: .pcr-page, .pcr-curl { transition: none } — states apply instantly, the reveal stays complete. One of the few effects in the catalog shipped with this rule.
  • Semantics: a native button whose aria-pressed toggles — announced as a toggle button, activatable with Enter and Space. But the aria-label replaces the content: a screen reader never hears “WELCOME20”, the reward is invisible to it. Put the code in the label once revealed (aria-label="Promo code WELCOME20, 20% off everything" when aria-pressed turns true), and translate it: the shipped HTML is in French. The three divs inside the button are invalid (a button only accepts phrasing content): replace them with spans set to display: block or grid.
  • Contrast measured (Chromium, pixels under the text): page text #cfcfe8 at 12.8 px on the #262640 → #16162a gradient = 9.6 to 11.6:1 (AAA); code #22ffaa at 20 px on #0d1512 = 14:1, 9:1 at the heart of the halo; sub-line #7fbfa8 at 10.9 px = 8.75:1, 5.6:1 on the halo — everything exceeds 4.5:1, no color to change. Only the 10.9 px size is small for useful information.
  • Keyboard: Tab reaches the button, :focus-visible draws outline: 2px solid #a78bfa at 3 px and curls the corner to 150 px — the same preview as hover; Enter or Space reveals. Touch: no hover, a tap reveals directly (the 150 px preview doesn't exist under a finger); target 250 × 160 px.
  • The button doesn't inherit the page font: without font: inherit, the text renders in the browser's default button font (Arial 13.33 px in Chromium) — rem sizes follow the root, the family doesn't. And the cursor stays default over a clickable card: add font: inherit; cursor: pointer.

Browser compatibility

Requires clip-path: polygon() with calc() and var(), transitions on clip-path, the inset shorthand and place-items — the limiting property is inset. ES5 JavaScript, zero dependencies.

Chrome 87+✓ Full
Firefox 66+✓ Full
Safari 14.1+✓ Full
Edge 87+✓ Full
Mobile iOS 14.5+✓ Full (tap reveals, no preview)
Android Chrome✓ Full (tap reveals, no preview)

Without JavaScript, hover and focus still curl the corner to 150 px, but nothing reveals the code: add a server-side fallback or show the code elsewhere. Without clip-path transitions (very old browsers), states jump at once; without inset, the three layers have neither size nor position and only the card background remains.

The code

Copy the three blocks into your page. No dependencies.

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');
    });
  });
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
--pcr-c at rest (CSS — .pcr-page and .pcr-curl) 34px Size of the curled corner at rest. 0 = flat page with no invitation; 48px = clearly visible corner. Set the same value on both elements: the variable is declared on each, not inherited between siblings.
--pcr-c on hover (CSS — :hover / :focus-visible, + fold width/height) 150px Size of the preview. The fold is opaque and lies on the page: at 150 px on a 250 px card it already covers the right half of the centered label (visible in the demo). Drop to 110 px to keep the label whole. Three values to change together: the page variable, the fold's width and height.
--pcr-c on click (CSS — [aria-pressed="true"], two rules) 380px Must be at least width + height of the card (410 px for 250 × 160) for the page to disappear; at 380 px a 26 px triangle remains bottom-left. Allow 10 px of margin.
Duration and curve (CSS — transition .55s cubic-bezier(.25,.8,.3,1)) .55s Decelerating curve: the fold starts fast and settles. .35s = crisp; .8s = heavy paper. The three transitions (clip-path, width, height) must stay in sync or the fold drifts off the cut.
Back of the page (CSS — .pcr-curl, linear-gradient 225deg) transparent 48% → #3d3d66 → #52528a 70% → #2c2c4d The fold's material. Lighten the 70% stop for glossy paper; keep the transparent stop under 50% so the empty triangle stays aligned with the cut.
Dimensions (CSS — .pcr-card width / height) 250 × 160 px Card format. Recompute the click value (width + height) and, on a wide card, the hover value. The layers follow through inset: 0.
Hidden content (HTML — .pcr-under) code + sub-line Free HTML in a centered grid: text, emoji, image. Everything in it is hidden on screen but present in the DOM — read by screen readers unless the aria-label overrides it, copyable with Ctrl+A.

FAQ

After the click, a small corner of the page stays visible bottom-left — why?

Geometry. The cut removes everything above the diagonal x − y = width − c; at c = 380px on 250 × 160, the diagonal passes through (0, 130) and (30, 160): the triangle below it, 26 px per leg, stays covered by the page. Measured in Chromium: 331 page pixels after the click. You need c ≥ width + height (410 px); raise both [aria-pressed="true"] rules to 420px.

The --pcr-c variable isn't registered with @property: how can the corner glide?

Because the variable isn't what's animated — clip-path, width and height are, and their computed values change when the variable changes. The browser interpolates two polygons with the same number of vertices — which is why the pentagon keeps five points even at rest. @property would only be needed to animate --pcr-c itself.

Can I reveal on hover alone, without a click — or remove the hover preview?

For a revealing hover: raise the :hover rules to --pcr-c: 420px and the fold to 420 px, drop the JS; the page comes back as soon as the mouse leaves. To remove the preview (touch behavior everywhere): delete the :hover rules and keep the :focus-visible ones. To close automatically: a setTimeout that sets aria-pressed back to false after a few seconds.