Click on SIGN UP button to trigger animation

Welcome back
Time to feel like home

New here?

Sign up and discover great amount of new opportunities!

One of us?

If you already has an account, just sign in. We've missed you!

Don't forget to check the tutorial below!

If this was a job project, then I would be using something like tailwind, or css-in-js tied to a specific UI library (like Mantine), or at the very least SCSS modules. But since it's a personal website, which is also meant to be a source of educational tutorials, I decided to go with SCSS BEM classes, since they are 100% compatible with codepen, which makes changing/copypasting code very simple. Also, SCSS got the broadest appeal, since it doesn't require any specific library knowledge to read the code. But I am not promising that my styles will be easy to understand :)
This website is meant to showcase various experiments with fun animations and relatively new CSS features, but it is not intended to be a source of production-ready components with mobile responsiveness and proper accessibility. If you can easily copy-paste some of these components into your app, then I am happy for you, but generally don't expect same level of DX/UX polish similar to popular libraries with huge support behind them.
At my job I try to evade heavily commenting my code, but since this website is meant to be the source of learning materials about UI front-end, I decided to add a variety of comments where it might seem useful.

Form Switch Animation Tutorial

What, Why, How?

This demo is a "remaster" of my old demo back from 2017. New version is most likely worse from UX and aethestic point of views, but this website is not about that. This website is about doing excessive stupid stuff with animations and cutting-edge tech, so don't seek any meaning here.

Original demo was based around 2 following animations:

  1. Moving image container (aka switcher) from side to side, while "revealing" background behind it.
  2. Swapping forms underneath image container, which makes it look like we are just replacing form content on the fly.

But it had one severe limitation - it required fixed width for main container, because it was the only way to know size of the background image for nested container. Limitation is tied to the fact that the image background is located inside of second-level child relative to main container, so we can't know the relative width of the main container, without fixed pixels size.

After playing around I finally found the solution - it was css clip-path! And using it led me to next idiotic addition - now I can use some goofy polygon shapes and animate them, because why not?

So let's dive in!

Show me the code

Tutorials will mostly be covering a general overview of the implementation with a few code embeds here and there. Full code with comments is always available on github (linked below) and codepen.

Also, if reading SCSS code with parent references nesting is too much trouble, you can always check compiled CSS in codepen to see the final classes and styles.



Rough explanation of layout and switch animation

Here is the initial view breakdown:

Initial view breakdown

Once SIGN UP button is pressed, the state changes and s--switched class is appended to demo element, which triggers following css transitions:

  1. Switcher element moves to the left and changes shape accordingly (more on that later).
  2. Forms container moves to the right by width of the switcher.
  3. Forms swapping their opacity and pointer-events right in the middle of animation with 0s animation duration, when switcher element is fully covering the form, which leaves no room for any visual glitches, even for few ms.

As for reverse animation, everything there works automatically when you are removing the class.

Switcher and clip-path

In the original codepen demo, switcher is just a normal container with overflow: hidden, inside of which we can put our content, and background is done via nested element (:before) with width equal to fixed demo container width, so that you could move it during animation in the opposite direction of switcher movement, to create an effect of "revealing" the background behind it. And fluid width container isn't possible with that implementation, since 2-level nested children cannot get relative width of top-level parent (unless I am missing something).

As you already know, the solution for having fluid width container is css clip-path. Please check MDN for full reference, but here is the quick tldr:

  • CSS clip-path is essentially a more DX friendly version of svg clipPath. It supports relative values (which is super amazing in our case) and comes with few special predefined shapes, like polygon, circle, ellipse (we are gonna use polygon).
  • You can also use path version, which works same way as path in svg, but it loses ability to use relative values like %.
  • It allows you to "clip" the content, same way as you would do it with overflow: hidden, let's say, but you are not limited to basic rectangular shapes of usual html layout.
  • And on top of it, you can change the coordinates for clipping area and everything will be automatically animated with css transitions!
  • Somewhere like 5-7 years ago you would need a custom canvas or animation library paired with change of svg clipPath to achieve what now you can do with just simple clip-path transition and different coordinates on a state class.

Here is the visual breakdown of switcher container:

Switcher container visual breakdown

Here you can see x values for initial and switched states:

.demo__switcher {
  --x1: calc(100% - var(--switcher-width));
  --x2: calc(100% - var(--arrow-offset));
  --x3: 100%;
  --x4: calc(100% - var(--arrow-offset));
  --x5: calc(100% - var(--switcher-width));
  --x6: calc(100% - var(--switcher-width) + var(--arrow-offset));

  // ... other styles
  clip-path: polygon(var(--x1) 0, var(--x2) 0, var(--x3) 50%, var(--x4) 100%, var(--x5) 100%, var(--x6) 50%);
  transition: clip-path var(--anim-time) var(--easing);
  will-change: clip-path;

  @include switched {
    --x1: var(--arrow-offset);
    --x2: var(--switcher-width);
    --x3: calc(var(--switcher-width) - var(--arrow-offset));
    --x4: var(--switcher-width);
    --x5: var(--arrow-offset);
    --x6: 0;

We'll also need to apply very similar clip-path values to main demo container, to match the arrow shape size, since otherwise we'll end up with white corners.

.demo__inner {
  --demoX1: 0;
  --demoX2: calc(100% - var(--arrow-offset));
  --demoX3: 100%;
  --demoX4: calc(100% - var(--arrow-offset));
  --demoX5: 0;
  --demoX6: 0;

  // ... other styles
  transition: clip-path var(--anim-time) var(--easing);
  will-change: clip-path;
  // clip the main container to match the arrow shape, otherwise there will be white corners
  clip-path: polygon(var(--demoX1) 0, var(--demoX2) 0, var(--demoX3) 50%, var(--demoX4) 100%, var(--demoX5) 100%, var(--demoX6) 50%);

  @include switched {
    --demoX1: var(--arrow-offset);
    --demoX2: 100%;
    --demoX3: 100%;
    --demoX4: 100%;
    --demoX5: var(--arrow-offset);
    --demoX6: 0;

Here is the rough explanation of how switcher text content is positioned and moved during the animation:

.demo__switcher-inner {
  /* I'm using this sub-container with full-width to allow animating switcher content position with transforms,
  instead of left/right, because of performance difference
  but transforms got severe limitation - relative % values are tied to element's own width/height,
  not parent's, so that's why we need this sub-container
  thanks to calc, we can shift it all the way to the left minus switcher width,
  which will end up an equivalent of animating from left: calc(100% - var(--switcher-width)) to left: 0 */
  height: 100%;
  transition: var(--transition-transform);
  will-change: transform;

  @include switched {
    transform: translateX(calc((100% - var(--switcher-width)) * -1));

.demo__switcher-content {
  // our content is always positioned in the same place on the right
  // so we are just moving its parent container instead, as explained above
  overflow: hidden;
  position: absolute;
  right: 0;
  top: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  column-gap: 20px;
  width: var(--switcher-width);
  height: 100%;

Once switcher code is ready, the core animation is pretty much done.

Additional notes:

  • Button with transparent background and animated border will be covered in a separate tutorial, stay tuned!
  • Originally when I started playing with clip-path, I wanted to make a skewed shape, which lead me to some overcomplicated code where I was using brand-new css trigonometric functions to compute angle with atan2 in order to determine skew angle for content beside the shape. And then it hit me that applying skew globally to the whole component was accomplishing the same, so I dropped that stupid idea. It is a good example of how everything becomes a nail when you are a hammer, when you are stuck in a tunnel-vision tied to some specific idea.
  • If any Tailwind folks are checking this, can you please ping me on twitter and show me how similar clip-path animation could be done with tailwind? Or it's just create custom class that does all the usual stuff?

Please follow me on twitter for my latest demos, tutorials and cooked takes.