I came across Kitty Giraudel’s folded corners technique. It leverages CSS clip-path, and I thought that that was such a cool way to do it. clip-path has been trending lately, most likely because web browsers support the shape() function now.
However, I’ve been on a bit of a corner-shape kick lately (have a look at my introduction to corner-shape as well as these scroll-driven corner-shape animations), so I figured that corner-shape could be used to create folded corners as well, and this is what I came up with:
So open Chrome, which supports corner-shape, and let’s dig in (if you’re looking at this in other browsers, it basically falls back to a rounded corner).
Step 1: Set some CSS variables
Elements have four corners, but when we use border-radius, each corner is split into two coordinates. The x-axis coordinate moves along the x-axis, away from its associated corner, while the y-axis coordinate does the same thing along the y-axis. It’s from these coordinates that border-radius draws the curvature of the rounded corners.
First, store the coordinates as CSS variables. We’ll need the values that they hold more than once, so this simplifies things, makes the fold animatable, and maintains some degree of realism.
Given what we now know about border-radius, it should be obvious what border-top-right-radius does. As for corner-top-right-shape: bevel, that ensures that a straight line is drawn between the coordinates instead of rounded corners (corner-top-right-shape: round). That’s right, border-radiusincludescorner-shape: round by default (behind the scenes, of course).
/* Square */
div {
/* Place coordinates */
border-top-right-radius: var(--x-coord) var(--y-coord);
/* Draw line between coordinates */
corner-top-right-shape: bevel;
}
Step 3: Creating the flip side
Now that we’ve established the fold, it’s time to create the flip side. Start by selecting ::before, then declare content: "" to create the element without content. The background can be inherited from the square, and the dimensions should leverage the coordinates that we saved. As you can see, I’ve also added a box-shadow where the blur radius scales with --x-coord and --y-coord, but you’re welcome to adapt the formula as you see fit.
/* Square */
div {
/* Place coordinates */
border-top-right-radius: var(--x-coord) var(--y-coord);
/* Draw line between coordinates */
corner-top-right-shape: bevel;
/* Flip side */
&::before {
/* Generate empty element */
content: "";
/* Inherit background */
background: inherit;
/* Same as coordinates */
width: var(--x-coord);
height: var(--y-coord);
/* Scale blur radius with --x-coord and --y-coord */
box-shadow: 0 0 calc((var(--x-coord) + var(--y-coord)) / 3) #00000050;
}
}
Step 4: Positioning the flip side (::before)
Next, we need to shift ::before to the (top-)right corner. We’re avoiding anchor positioning, because there’s no need for modern features if more supported features work well using the same amount of code. So, declare position: relative on the square and position: absolute on ::before. This makes ::before position relative to the square, and is a trick that only works for parent-child relationships. Actually, this shortcoming is why anchor positioning was invented, but we just don’t need it in this case.
In addition, declare inset: 0 0 auto auto on ::before to align it to the top-right corner of the square, and overflow: clipon the square to clip the half of ::before that overflows it.
/* Square */
div {
/* Place coordinates */
border-top-right-radius: var(--x-coord) var(--y-coord);
/* Draw line between coordinates */
corner-top-right-shape: bevel;
/* Clip any overflow */
overflow: clip;
/* For alignment */
position: relative;
/* Flip side */
&::before {
/* Generate empty element */
content: "";
/* Inherit background */
background: inherit;
/* Same as coordinates */
width: var(--x-coord);
height: var(--y-coord);
/* Scale blur radius with --x-coord and --y-coord */
box-shadow: 0 0 calc((var(--x-coord) + var(--y-coord)) / 3) #00000050;
/* For alignment */
position: absolute;
/* Align to top-right */
inset: 0 0 auto auto;
}
}
You can stop here if you want, but there’s room for improvement…
Step 5: Sculpting the flip side
To make the outcome look a bit more realistic, we’ll use corner-bottom-left-shape: bevel to make one more straight cut, this time to ::before. There are, most likely, many ways to tackle this depending on how sharply we want to crease the fold, how elevated we want the flip side to be, and the angle from which we want to view the square, but I don’t think it matters as long as the effect looks decent, so we’re aiming for a sharp crease, the flip side sticking up, and an aerial view. If you’d rather something different, keep in mind that the shadow also impacts the outcome, and that you’d be facing a trickier implementation.
The only degree of complexity that I suggest is this:
These are container style queries using the range syntax, where if the value of --x-coord is less than the value of --y-coord, we subtract the value of --x-coord from 100% and use it as the y-axis coordinate for the relevant border radius (border-bottom-left-radius, in this case). The other axis is set to 100%. Adversely, if the value of --x-coord is more than (or equal to) the value of --y-coord, we subtract the value of --y-coord from 100% and use it as the x-axis coordinate. Once again, the other axis is set to 100%.
The result is that the crease, shadow, and now perspective of the fold is calculated using only --x-coord and --y-coord to look realistic (or realistic enough, anyway). Using the slideVars toggles in the top-right corner of the demo, you can see for yourself by testing various combinations of coordinates:
If you want to implement a failsafe to ensure that the coordinates don’t exceed the dimensions of the square, breaking the effect, you can use min(). The modified coordinate variables below set --y-coord to an impossible 999999999rem, but caps it at the height of the square (although I can’t imagine that you’d actually need this, to be completely honest):
Kitty’s Giraudel’s folded corners work in all browsers, and because clip-path is used, which is a more versatile shaping feature, there are more ways to customize the shape. It’s also the more correct approach, for whatever that’s worth. However, my corner-shape approach is cleaner and likely wouldn’t require any further customization anyway, but lacks Safari and Firefox support for now. So unless you need folded corners today, I’d bookmark both:
My mum loved logic because she was born at a time when nothing made sense. She was born in 1945, the year World War II ended, so she dodged a literal bullet because we are Jewish. But from the first day of her life, she found that famine, racism, and misfortune kept trying to take her away. In 2011, cancer took her away from me forever. But on a lighter note, this Mother’s Day I’m bringing her back to life the only way I know how: UI mad science!
I will explain how my mum inspired this 2026 Mother’s Day scrollytelling experiment — but also, how she inspired my approach to dev and life. Along the way, I’ll discuss some of the tech involved in this virtual Mother’s Day gift. I normally write either inspirational or technical posts — but for Mother’s Day, you’re getting a twofer.
Alternatively, here’s a video demo with commentary by my eight-year-old. It was bittersweet to realise that this is the closest he has come to interacting with his nana, because she passed before he was born.
Why I made this
Mum was born in a hospital in Kazakhstan, where civilian patients shared wards with discharged soldiers suffering PTSD. They wandered in and out of the maternity rooms, terrifying the patients and making labour even harder for my grandmother.
When Mum was born, she wasn’t breathing. The staff immersed her in cold water, then hot, then cold water again — a so-called remedy at the time based on no science. This was the beginning of a larger pattern in her life: She kept surviving not because of the help she received, but despite chaos disguised as help.
So, as an adult, Mum learned to survive by finding patterns and sense in the unfathomable. She accomplished this by combining her three passions:
In photography, she framed moments when the chaos of her surroundings temporarily harmonized into beauty.
In teaching, she used those images to help tell a story that broke the chaos into logical steps people could follow.
In computer programming, she encapsulated those illustrated teachable moments within interactive experiences. Unlike in real life, if a programmed interaction goes wrong, you can trace why and solve the problem.
In other words, she educated me by using the skill set I now think of as web development—before the web existed.
Gamifying the experience of knowing my mum
I drew inspiration from Roland Franke’s deconstructed radial slice transition using scroll-snap events. Roland’s Pen showcases eye-catching, scroll-triggered transitions between landscapes as a figure sits in the foreground watching. This made me think of the patience my mum put into observing the world—but then she’d encapsulate everything in short, interactive stories I could digest as a young child.
I’m symbolizing that experience in my Mother’s Day game with the scroll-triggered time-lapse animation of day to night, stylized with CSS shapes. Using a single scroll gesture, we grasp the gist of an entire day. That experience is like the way my mum could explain a big topic to me in a way that felt like play.
My mum taught me that video games don’t have to be about blowing things up. She once used QuickBASIC to build a photography game long before Pokémon Snap existed. I remember passing a shop in the 90s with Armor Alley playing in demo mode in the window. I was obviously fascinated, but my mum said, “I don’t like it. The helicopter started it,” then she went home and built her photography game for me to play instead.
She once told me a story about photography from her childhood in the Soviet Union. She remembered taking a photo of a government building just because it looked cool, but a soldier saw her and confiscated the roll of film from her camera. Maybe the lesson was that exposing the reality of something can be just as much of a threat as shooting things in the militaristic sense of the word.
The violence common in games is a metaphor for the uncertainty and randomness we face in life, but my mum’s photography game taught me that violence isn’t the only way of coping with that problem, even in a game.
How the scrollytelling Mother’s Day card works
My mum inspired the randomness of the UFOs in this experiment with her ability to use a camera to capture the fleeting moments of sense in a chaotic world.
The combination of deterministic scroll-triggered animations with the randomness of the UFOs and text physics is possible using alien technology I’ve not seen used much in the wild: scroll-snap events. This emergent module — available only in Chrome and Opera at the time of writing — provides a simple JavaScript API so that when we style the page to snap between the day and night scenes, we can trigger behavior that isn’t possible in CSS alone, like the random flight paths of the UFOs, and the Pretext-inspired effect of the UFOs repelling letters as the spaceships fly through the text.
/* The scroll container */
body {
overflow-y: auto;
scroll-snap-type: y mandatory;
}
/* Each snap target */
.snap-panel {
scroll-snap-align: start;
scroll-snap-stop: always;
}
…and the JavaScript to handle scroll snap events:
// scrollsnapchanging fires while the user is scrolling —
// snapTargetBlock is the panel they are heading toward.
snapScroller.addEventListener('scrollsnapchanging', ({ snapTargetBlock }) => {
markPanelStates({ active: selectedPanel, incoming: snapTargetBlock });
if (snapTargetBlock === dayPanel) onScrollingTowardDay();
if (snapTargetBlock === nightPanel) onScrollingTowardNight();
});
// scrollsnapchange fires once a panel has snapped into place —
// snapTargetBlock is the panel now fully in view.
snapScroller.addEventListener('scrollsnapchange', ({ snapTargetBlock }) => {
selectedPanel = snapTargetBlock;
markPanelStates({ active: selectedPanel, incoming: null });
if (snapTargetBlock === dayPanel) onLandedOnDay();
if (snapTargetBlock === nightPanel) onLandedOnNight();
});
You can see that handling these events lets us create context-aware transitions between the two scenes, depending on the state of the game logic when the user slides between day and night and back again. My mum would always give me another chance to get things right.
In case you found this while Googling for a conventional Mother’s Day gift idea…
Parting words: In the unlikely event that your mum is not into avant-garde homemade virtual gifts that showcase emergent browser features, get her a Kindle or something. I bought my mum a Kindle when she was alive. We’d read the same novel separately on our Kindles, then we’d compare notes over the phone on the days I couldn’t visit.
As a Chrome user, you’ll have received Gemini Nano in the form of a 4GB transfer recently; no permission asked or required. If you remove it, Chrome will re-download it. For reasons I can only guess at, Gemini Nano is presumably now considered to be part of Chrome itself, despite being a standalone product that is included alongside but not integrated into the browser — the way a copy of Bonzi Buddy included in a browser update might be considered a part of said browser.
Do not engage … generating or distributing content that facilitates … Sexually explicit content Do not engage in misinformation, misrepresentation, or misleading activities. This includes … Facilitating misleading claims related to governmental or democratic processes
This seems like a bad direction for an API on the web platform, and sets a worrying precedent for more APIs that have UA-specific rules around usage.
I have nothing to add, only that this is the sort of thing that seems worth knowing. Mat’s take-home isn’t exactly comforting because, remember, this has already shipped:
I’d like to say that something to the tune of “their whole argument hinges on ‘positive developer sentiment,’ so let’s show them that there isn’t any” — but there isn’t any; they cited places where there isn’t any. That’s not how it works for them. Google participates in the web standards process the way a bear participates in the “camping” process.
[…]
Remember this the next time Google announces an “exciting new standard” that they’re heroically championing — for you, for users, for good of the web — in language that has just a hint of inevitability about it.