Back to Blog

March 12, 2026

Crafting a 60° Slanted Page Transition

The journey of building a diagonal wipe page transition that matches my personal brand—from clip-path polygons to trigonometry and back again.

My logo has a distinctive slant. It’s not quite a triangle, not quite a parallelogram—it’s a sharp 60° diagonal that cuts through the composition. When I redesigned my portfolio, I knew the page transitions had to echo that same geometry. A standard fade or slide wouldn’t do. I needed a slanted wipe.

This is the story of how that seemingly simple idea turned into a deep dive into CSS clip-path, browser quirks, and the mathematics of right triangles.

The Vision

The concept was straightforward: when navigating between pages, the old content should wipe away with a diagonal edge, and the new content should reveal itself with the same 60° angle. Like a theatrical curtain with a sharp, modern twist.

Visually, I wanted this:

  • Forwards navigation (clicking a link): Wipe from right to left
  • Back navigation (browser back button): Wipe from left to right
  • The angle: Exactly 60° from horizontal, matching my logo

First Attempt: Hardcoded Percentages

I started with Astro’s View Transitions API, which makes page transitions declarative. You define keyframe animations and assign them to old and new page elements.

The weapon of choice for diagonal reveals is clip-path: polygon(). By defining four coordinates, you can mask any rectangular element into any quadrilateral.

My first naive approach looked like this:

@keyframes slantedWipeOut {
  0% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }
  100% { clip-path: polygon(0 0, 0 0, 0 100%, 0 100%); }
}

That creates a vertical wipe. To get the slant, I needed to extend the top-right and bottom-left corners beyond the element’s bounds using negative percentages:

@keyframes slantedWipeOut {
  0% { clip-path: polygon(-40% 0, 100% 0, 140% 100%, 0 100%); }
  100% { clip-path: polygon(-40% 0, -40% 0, 0 100%, 0 100%); }
}

The -40% and 140% values were arbitrary. They looked “slanty enough” on my laptop. I shipped it and moved on.

The Aspect Ratio Problem

Later that week, I tested the site on an ultrawide monitor. The angle looked… off. Flatter than 60°. Then I checked on a phone in landscape mode. Steeper than 60°.

The problem: percentages in clip-path are relative to the element’s width for horizontal coordinates and height for vertical coordinates. My -40% offset meant “40% of the viewport width,” but the height determines the angle.

On a 16:9 screen, 40% of width relative to height creates one angle. On a 21:9 ultrawide, that same 40% is proportionally different relative to the height. The angle changes with the aspect ratio.

Enter Trigonometry

To maintain exactly 60° on any screen size, I needed to calculate the horizontal offset based on the viewport height:

tan(60°) = opposite / adjacent
tan(60°) = height / horizontal_offset
horizontal_offset = height / tan(60°)

As a percentage of viewport width:

offset = (100vh / tan(60°) / 100vw) × 100%

Modern CSS supports trigonometric functions, so my first instinct was:

--slant-offset: calc(100vh / tan(60deg) / 100vw * 100%);

Then use it everywhere:

clip-path: polygon(
  calc(-1 * var(--slant-offset)) 0,
  100% 0,
  calc(100% + var(--slant-offset)) 100%,
  0% 100%
);

It worked! On paper. In reality, Safari 15 and older Chrome versions don’t support tan() in CSS. The transition just… didn’t happen. A silent failure.

The CSS Variable Approach

I needed the browser to compute this value without trig functions. The formula 100vh / 1.73205080757 / 100vw * 100% works purely with division and multiplication.

I defined a CSS custom property:

@property --slant-offset {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 40%;
}

:root {
  --slant-offset: calc(100vh / 1.73205080757 / 100vw * 100%);
}

The @property definition ensures browsers treat this as a valid interpolatable value for animations. Without it, some browsers can’t animate clip-path smoothly when it contains a var().

This approach works everywhere clip-path is supported, with no JavaScript needed.

The Forward and Backward Dance

Page transitions in Astro have two directions: forwards (navigating to a new page) and backwards (using the back button). Each needs its own choreography.

Forwards: The old page wipes out to the left while the new page wipes in from the right. Both share the same slant angle, creating the illusion of a single diagonal blade sweeping across the screen.

Backwards: The inverse. Old page wipes to the right, new page enters from the left.

But here’s the subtle part: the old page animations (slantedWipeOut and slantedWipeOutReverse) aren’t just clearing the screen. They’re revealing another layer beneath—creating two parallel slanted lines that move in tandem. Think of it as a venetian blind effect with diagonal slats. As the old page exits, a second transition layer peeks through the gap, adding depth and visual interest.

The key is symmetry. The exit animation of the old page must perfectly match the entry animation of the new page at the handoff point. Any mismatch creates a visual pop or gap.

/* Forwards: old page collapses to left */
@keyframes slantedWipeOut {
  0% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, 100% 0, calc(100% + var(--slant-offset)) 100%, 0% 100%); }
  100% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, calc(-1 * var(--slant-offset)) 0, 0% 100%, 0% 100%); }
}

/* Forwards: new page expands from right */
@keyframes slantedWipeIn {
  0% { clip-path: polygon(100% 0, 100% 0, calc(100% + var(--slant-offset)) 100%, calc(100% + var(--slant-offset)) 100%); }
  100% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, 100% 0, calc(100% + var(--slant-offset)) 100%, 0% 100%); }
}

Notice the final frame of slantedWipeOut matches the initial frame of slantedWipeIn. That seamless handoff is what makes the transition feel polished.

Timing Is Everything

The durations matter as much as the geometry. I settled on:

  • Forwards old: 0.3s (get out of the way quickly)
  • Forwards new: 0.5s (take your time arriving)
  • Backwards old: 0.2s (even faster, back button feels urgent)
  • Backwards new: 0.5s (same elegant entrance)

All use the same easing curve: cubic-bezier(0.22, 1, 0.36, 1)—an ease-out-quint that starts fast and settles gently. It feels responsive without being jarring.

The Logo Connection

The 60° angle isn’t arbitrary. It’s the same angle in my logo’s diagonal stroke. When someone navigates my site, they’re literally seeing my brand geometry in motion.

That consistency between static identity (logo) and dynamic behavior (transitions) creates a cohesive experience. Users might not consciously notice the angle matches, but they feel the intentionality.

Final Implementation

The complete implementation lives in my root layout:

---
const slantedWipe = {
  forwards: {
    old: { name: "slantedWipeOut", duration: "0.3s", easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
    new: { name: "slantedWipeIn", duration: "0.5s", easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
  },
  backwards: {
    old: { name: "slantedWipeOutReverse", duration: "0.2s", easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
    new: { name: "slantedWipeInReverse", duration: "0.5s", easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
  },
};
---

<html transition:animate={slantedWipe}>
  <!-- ... -->
</html>

<style is:global>
  @property --slant-offset {
    syntax: "<length-percentage>";
    inherits: true;
    initial-value: 40%;
  }
  
  :root {
    --slant-offset: calc(100vh / 1.73205080757 / 100vw * 100%);
  }

  @keyframes slantedWipeOut {
    0% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, 100% 0, calc(100% + var(--slant-offset)) 100%, 0% 100%); }
    100% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, calc(-1 * var(--slant-offset)) 0, 0% 100%, 0% 100%); }
  }

  @keyframes slantedWipeIn {
    0% { clip-path: polygon(100% 0, 100% 0, calc(100% + var(--slant-offset)) 100%, calc(100% + var(--slant-offset)) 100%); }
    100% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, 100% 0, calc(100% + var(--slant-offset)) 100%, 0% 100%); }
  }

  @keyframes slantedWipeOutReverse {
    0% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, 100% 0, calc(100% + var(--slant-offset)) 100%, 0% 100%); }
    100% { clip-path: polygon(100% 0, 100% 0, calc(100% + var(--slant-offset)) 100%, calc(100% + var(--slant-offset)) 100%); }
  }

  @keyframes slantedWipeInReverse {
    0% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, calc(-1 * var(--slant-offset)) 0, 0% 100%, 0% 100%); }
    100% { clip-path: polygon(calc(-1 * var(--slant-offset)) 0, 100% 0, calc(100% + var(--slant-offset)) 100%, 0% 100%); }
  }
</style>

What I Learned

  1. Clip-path percentages are relative to different dimensions (width for x, height for y), which makes consistent angles surprisingly tricky.

  2. CSS tan() is modern but not universal. Sometimes math constants are more compatible than math functions.

  3. The @property rule matters for animating custom properties smoothly. Without it, browsers can’t interpolate values in clip-path.

  4. Seamless transitions require matching handoff points. The end of one animation must exactly match the start of the next.

  5. Brand consistency extends to motion. A logo isn’t just a static image—its geometry can inform the entire interaction design.

The slanted transition is now live across my entire portfolio. Click around. Notice how the diagonal edge feels sharp, intentional, unmistakably “mine.” That’s the power of sweating the small geometric details.

Happy slanting.