Recreate the Cyberpunk 2077 Button Glitch Effect in CSS

Jhey Tompkins
Jhey Tompkins
Share

If you’re interested in video games in the slightest, you’re no doubt aware of Cyberpunk 2077. It’s one of the most anticipated games of 2020. The world it paints has a certain style to it. The game’s website does a brilliant job of portraying that aesthetic. Its design does a great job of communicating the look and feel. As you can imagine, that means it has some rather slick-looking UI components.

Someone first reached out to me asking how I would create an image effect used on the site. If you hover images in the image galleries, they have this neat “noise” effect.

A shimmering noise effect when hovering over a scene

I accepted the challenge. I dug in to take a look at the site’s source. After some digging, I discovered it was being implemented with shaders and WebGL. I’m completely new to writing shaders and WebGL. This did spur me to give it a try. But, for now, I’ve put learning WebGL and shader code on the backburner.

What did catch our eyes as I proceeded to look around the site on my live stream was the neat glitchy effect buttons. I’m no stranger to creating glitchy effects with CSS. We decided I’d attempt to recreate them.

A shimmering AVAILABLE NOW button

And here’s how you can do it!

Effect Button

Let’s start with some markup:

<button class="cybr-btn">
  Beginning_
</button>

The things we need to get sorted first are sizing, color, and font. The best way to get these right? Dive into the source and see how it’s done. From the first inspection, we see that a custom font is being used. (You can see the direct link to it in the code block below.)

Let’s create a custom @font-face rule:

@font-face {
  font-family: Cyber;
  src: url("https://assets.codepen.io/605876/Blender-Pro-Bold.otf");
  font-display: swap;
}

Once we have that, we can put the basic styling in place. Using CSS variables for things like color and font size gives us opportunities later. This is also the reason for using the HSL color space. We’ll show why later.

--primary: hsl(var(--primary-hue), 85%, calc(var(--primary-lightness, 50) * 1%));
--shadow-primary: hsl(var(--shadow-primary-hue), 90%, 50%);
--primary-hue: 0;
--primary-lightness: 50;
--color: hsl(0, 0%, 100%);
--font-size: 26px;
--shadow-primary-hue: 180;

Putting that together gives us this starting point. Notice how we use an inset box shadow instead of a border for that blue line? That’s because a border would knock our text off center. The inset box shadow won’t affect text alignment.

See the Pen CSS Cyberpunk 2077 Button: Beginning by SitePoint (@SitePoint) on CodePen.

The clipped corner

A noticeable feature of the button is that clipped corner. My first thought here is to use a clip path. But, to my surprise, the shape of the buttons on the site is being achieved with a background image.

We can clip the corner using the clip-path property:

clip-path: polygon(-10% -10%, 110% -10%, 110% 110%, 10% 110%, -10% 40%);

Note how we’re not clipping to the edges of the button. We’re giving the button 10% of breathing room. That’s because we need to account for the “R25” tag and the fact that the glitchy effect flows outside of the button. That’s a neat trick with clip-path. We can use it as a controlled overflow: hidden. We’re saying, “Yeah, you can overflow a little. But only this much”.

Adding that to our button gives us the clipped effect we desire.

See the Pen CSS Cyberpunk 2077 Button: Clipping by SitePoint (@SitePoint) on CodePen.

Creating the R25 tag

Next, let’s create that “R25” tag. We could reach for a pseudo element here and use the content property. In fact, this is how it’s done on the site. But there’s something to be mindful of with this approach — the fact that a screen reader might read it out. The same goes for the actual button text. Each button on the site has text succeeded by an underscore. Would we want that read out by a screen reader? If yes, then we can leave it as is. Let’s assume they’re for decorative purposes. We can update our markup and use aria-hidden so that a screen reader only reads the button’s text:

<button class="cybr-btn">
  Clipped<span aria-hidden>_</span>
  <span aria-hidden class="cybr-btn__tag">R25</span>
</button>

To style the tag, we can give it absolute positioning. This requires us to set relative positioning on the button. Like the button itself, the tag uses an inset box-shadow:

.cybr-btn {
  --label-size: 9px;
  --shadow-secondary-hue: 60;
  --shadow-secondary: hsl(var(--shadow-secondary-hue), 90%, 60%);
  position: relative;
}
.cybr-btn__tag {
  position: absolute;
  padding: 1px 4px;
  letter-spacing: 1px;
  line-height: 1;
  bottom: -5%;
  right: 5%;
  background: var(--shadow-secondary);
  color: hsl(0, 0%, 0%);
  font-size: var(--label-size);
  box-shadow: 2px 0 inset var(--shadow-primary);
}

We’ve introduced some more CSS variables here. Although they’re being used by the tag, we’ve put them under the button selector. There’s a reason for this. We may decide to leverage the power of scoped variables later. If we do, we only need to set the variables on the button selector. If we left the variables under the tag rule, variables set on the button wouldn’t have power over the lower scope. We set a background-color for the tag. But it soon becomes apparent that this isn’t being done on the site.

With our tag in place, the button is now taking shape.

See the Pen CSS Cyberpunk 2077 Buttons: Tagging by SitePoint (@SitePoint) on CodePen.

Adding the glitch effect

It’s time for the glitch effect. From experience, my assumption here was that the button was being duplicated. The duplicated button would have some form of clipping animation applied. Our first task here would be to create the glitch body. Remember we discovered the use of a background image earlier? It soon became clear why that was being used: to provide a cutout for the tag. That means the background-color behind the button is the same for the tag. The corner cut out is also created with the image.

red background

Notice how the blue border follows the corner and goes around the “R25”? Using a clip path as we have cuts that corner off and doesn’t outline the “R25”. The site’s implementation uses a drop-shadow.

Using a background image will allow us to recreate the effect. It comes with some compromises, though, if we want to make our buttons flexible and reusable.

For example, what if we want to change the color of the button? Do we have to create many images for each button color variant? What if we change the aspect ratio of the button? The image won’t fit anymore.

The glitchy animation is quick. It’s quick enough that it’s unlikely the clipped corner would be noticeable. That trade-off is worth it for a more flexible and reusable set of styles.

Let’s proceed with that solution. We can add a new element for the glitch. This needs the same text as our button and also needs hiding from the screen reader with aria-hidden:

<button class="cybr-btn">
  Glitch<span aria-hidden>_</span>
  <span aria-hidden class="cybr-btn__glitch">Glitch_</span>
  <span aria-hidden class="cybr-btn__tag">R25</span>
</button>

We need to duplicate the text here, and we have options. The site uses a pseudo element for duplicating the text. But if we do this, it means animating two elements at once for the effect. By moving the text into the glitch element, we only need to animate one element:

.cybr-btn__glitch {
  position: absolute;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
  box-shadow: 0 0 0 4px var(--shadow-primary);
  text-shadow: 2px 2px var(--shadow-primary), -2px -2px var(--shadow-secondary);
}

Applying some styles such as text-shadow and a box-shadow get us here.

See the Pen CSS Cyberpunk 2077 Buttons: Glitch Body by SitePoint (@SitePoint) on CodePen.

But we’re not satisfied with that corner clipping. Also, how we’re using clip-path to give breathing room feels brittle. We could get it back with a little trick. If we use pseudo elements to color the button, we won’t have to clip the entire button! We could use absolute positioning and then clip only the pseudo elements. We also won’t need to provide breathing room. The bonus here as well is that we already have the button colors in variables:

.cybr-btn {
  --clip: polygon(0 0, 100% 0, 100% 100%, 8% 100%, 0 70%);
}
.cybr-btn:before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--primary);
  clip-path: var(--clip);
  z-index: -1;
}

We can remove clip-path from the button and put that clip into a variable that we can reuse. We need to apply z-index: -1 to the pseudo elements so that the text still shows:

.cybr-btn {
  --border: 4px;
}

.cybr-btn__glitch {
  position: absolute;
  top: calc(var(--border) * -1);
  left: calc(var(--border) * -1);
  right: calc(var(--border) * -1);
  bottom: calc(var(--border) * -1);
  background: var(--shadow-primary);
  text-shadow: 2px 2px var(--shadow-primary), -2px -2px var(--shadow-secondary);
  clip-path: var(--clip);
}

.cybr-btn__glitch:before {
  content: '';
  position: absolute;
  top: calc(var(--border) * 1);
  right: calc(var(--border) * 1);
  bottom: calc(var(--border) * 1);
  left: calc(var(--border) * 1);
  clip-path: var(--clip);
  background: var(--primary);
  z-index: -1;
}

We can then reuse the clip for the glitch element’s pseudo element. The trick to getting the glitch element correct is to position it absolutely as if it’s the border. Then overlay the pseudo element on top of it. Applying the same clip to both elements will give us the neat blue border that follows the corner.

See the Pen CSS Cyberpunk 2077 Buttons: Reworked Glitch by SitePoint (@SitePoint) on CodePen.

How nifty is that? We can even adjust the clip path to get that cutout around the “R25”. Adjust the clip-path and remove the tag styles like this:

.cybr-btn {
  --clip: polygon(0 0, 100% 0, 100% 100%, 95% 100%, 95% 90%, 85% 90%, 85% 100%, 8% 100%, 0 70%);
}

.cybr-btn__tag {
  position: absolute;
  padding: 1px 4px;
  letter-spacing: 1px;
  line-height: 1;
  bottom: -5%;
  right: 5%;
  color: hsl(0, 0%, 0%);
  font-size: var(--label-size);
}

See the Pen CSS Cyberpunk 2077 Buttons: Clipping the tag by SitePoint (@SitePoint) on CodePen.

And this is where we have the opportunity to do something else cool. When I investigated the button and discovered the background image, I pulled it down. And what I found was that the border was possible by stacking two images and translating the bottom one. Now that we’re using clip-path, we can do the same.

Two stacked images

If we use the :before pseudo element for our button’s blue color and the :after for the red, then translate the :before pseudo element by the border size, it will give us the border. It gives us the border without applying a border:

.cybr-btn:after,
.cybr-btn:before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  clip-path: var(--clip);
  z-index: -1;
}

.cybr-btn:before {
  background: var(--shadow-primary);
  transform: translate(var(--border), 0);
}

.cybr-btn:after {
  background: var(--primary);
}

Now we have the shadow for the tag and the button. And the tag will use the background-color behind it. Try changing the background-color for the body and you’ll see!

See the Pen CSS Cyberpunk 2077 Buttons: Stacking Border by SitePoint (@SitePoint) on CodePen.

Animating the button

Almost there! Hang in. We have the glitch. We’ve got everything we need. All that’s left is to animate the button on :hover.

How is that glitch effect happening? The trick is to only show the glitch element on :hover and by default have an animation applied to it. My assumption here was the use of transform and clip-path in a set of keyframes. And I was right! How did I find out? I inspected the button and used Chrome’s “force state” to set the button to the :hover state.

Forcing the state in the inspector

Then, inspect the styles and find the animation. Click the filename and that will take you to the source.

The source code for the animation

That allowed me to see the keyframes that were being used:

@keyframes glitch-anim-1 {
    0% {
        opacity: 1;
        -webkit-transform: translateZ(0);
        transform: translateZ(0);
        -webkit-clip-path: polygon(0 2%,100% 2%,100% 5%,0 5%);
        clip-path: polygon(0 2%,100% 2%,100% 5%,0 5%)
    }

    2% {
        -webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        -webkit-transform: translate(-5px);
        transform: translate(-5px)
    }

    6% {
        -webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        -webkit-transform: translate(5px);
        transform: translate(5px)
    }

    8% {
        -webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        -webkit-transform: translate(-5px);
        transform: translate(-5px)
    }

    9% {
        -webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
        -webkit-transform: translate(0);
        transform: translate(0)
    }

    10% {
        -webkit-clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
        clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
        -webkit-transform: translate3d(5px,0,0);
        transform: translate3d(5px,0,0)
    }

    13% {
        -webkit-clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
        clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
        -webkit-transform: translateZ(0);
        transform: translateZ(0)
    }

    13.1% {
        -webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
        clip-path: polygon(0 0,0 0,0 0,0 0);
        -webkit-transform: translate3d(5px,0,0);
        transform: translate3d(5px,0,0)
    }

    15% {
        -webkit-clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
        clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
        -webkit-transform: translate3d(5px,0,0);
        transform: translate3d(5px,0,0)
    }

    20% {
        -webkit-clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
        clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
        -webkit-transform: translate3d(-5px,0,0);
        transform: translate3d(-5px,0,0)
    }

    20.1% {
        -webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
        clip-path: polygon(0 0,0 0,0 0,0 0);
        -webkit-transform: translate3d(5px,0,0);
        transform: translate3d(5px,0,0)
    }

    25% {
        -webkit-clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
        clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
        -webkit-transform: translate3d(5px,0,0);
        transform: translate3d(5px,0,0)
    }

    30% {
        -webkit-clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
        clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
        -webkit-transform: translate3d(-5px,0,0);
        transform: translate3d(-5px,0,0)
    }

    30.1% {
        -webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
        clip-path: polygon(0 0,0 0,0 0,0 0)
    }

    35% {
        -webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        -webkit-transform: translate(-5px);
        transform: translate(-5px)
    }

    40% {
        -webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        -webkit-transform: translate(5px);
        transform: translate(5px)
    }

    45% {
        -webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        -webkit-transform: translate(-5px);
        transform: translate(-5px)
    }

    50% {
        -webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
        -webkit-transform: translate(0);
        transform: translate(0)
    }

    55% {
        -webkit-clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
        clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
        -webkit-transform: translate3d(5px,0,0);
        transform: translate3d(5px,0,0)
    }

    60% {
        -webkit-clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
        clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
        -webkit-transform: translateZ(0);
        transform: translateZ(0);
        opacity: 1
    }

    60.1% {
        -webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
        clip-path: polygon(0 0,0 0,0 0,0 0);
        opacity: 1
    }

    to {
        -webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
        clip-path: polygon(0 0,0 0,0 0,0 0);
        opacity: 1
    }
}

For our animation, we can follow the same structure. But in our example, we can apply different versions of our clip path:

.cybr-btn {
  --shimmy-distance: 5;
  --clip-one: polygon(0 2%, 100% 2%, 100% 95%, 95% 95%, 95% 90%, 85% 90%, 85% 95%, 8% 95%, 0 70%);
  --clip-two: polygon(0 78%, 100% 78%, 100% 100%, 95% 100%, 95% 90%, 85% 90%, 85% 100%, 8% 100%, 0 78%);
  --clip-three: polygon(0 44%, 100% 44%, 100% 54%, 95% 54%, 95% 54%, 85% 54%, 85% 54%, 8% 54%, 0 54%);
  --clip-four: polygon(0 0, 100% 0, 100% 0, 95% 0, 95% 0, 85% 0, 85% 0, 8% 0, 0 0);
  --clip-five: polygon(0 0, 100% 0, 100% 0, 95% 0, 95% 0, 85% 0, 85% 0, 8% 0, 0 0);
  --clip-six: polygon(0 40%, 100% 40%, 100% 85%, 95% 85%, 95% 85%, 85% 85%, 85% 85%, 8% 85%, 0 70%);
  --clip-seven: polygon(0 63%, 100% 63%, 100% 80%, 95% 80%, 95% 80%, 85% 80%, 85% 80%, 8% 80%, 0 70%);
}

@keyframes glitch {
  0% {
    clip-path: var(--clip-one);
  }
  2%, 8% {
    clip-path: var(--clip-two);
    transform: translate(calc(var(--shimmy-distance) * -1%), 0);
  }
  6% {
    clip-path: var(--clip-two);
    transform: translate(calc(var(--shimmy-distance) * 1%), 0);
  }
  9% {
    clip-path: var(--clip-two);
    transform: translate(0, 0);
  }
  10% {
    clip-path: var(--clip-three);
    transform: translate(calc(var(--shimmy-distance) * 1%), 0);
  }
  13% {
    clip-path: var(--clip-three);
    transform: translate(0, 0);
  }
  14%, 21% {
    clip-path: var(--clip-four);
    transform: translate(calc(var(--shimmy-distance) * 1%), 0);
  }
  25% {
    clip-path: var(--clip-five);
    transform: translate(calc(var(--shimmy-distance) * 1%), 0);
  }
  30% {
    clip-path: var(--clip-five);
    transform: translate(calc(var(--shimmy-distance) * -1%), 0);
  }
  35%, 45% {
    clip-path: var(--clip-six);
    transform: translate(calc(var(--shimmy-distance) * -1%));
  }
  40% {
    clip-path: var(--clip-six);
    transform: translate(calc(var(--shimmy-distance) * 1%));
  }
  50% {
    clip-path: var(--clip-six);
    transform: translate(0, 0);
  }
  55% {
    clip-path: var(--clip-seven);
    transform: translate(calc(var(--shimmy-distance) * 1%), 0);
  }
  60% {
    clip-path: var(--clip-seven);
    transform: translate(0, 0);
  }
  31%, 61%, 100% {
    clip-path: var(--clip-four);
  }
}

This is the trickiest part to comprehend. What is actually happening here? Our keyframes animate a clip path on the glitch element. At the same time, we shimmy the element from side to side. We can slow down the animation to see what’s happening.

Here’s a slowed-down demo to show how the animation is working:

See the Pen CSS Cyberpunk 2077 Buttons: Slowed down animation by SitePoint (@SitePoint) on CodePen.

And I’ve also put together a demo that shows the different states of the clip:

See the Pen CSS Cyberpunk 2077 Buttons: Clipping States by SitePoint (@SitePoint) on CodePen.

This would make it much easier for us to maintain and tweak the different animation states.

Connecting with hover

All that’s left to do is tie this up to the :hover selector. By default, we hide the glitch element. Then, on hover, we show it animating:

.cybr-btn__glitch {
  display: none;
}
.cybr-btn:hover .cybr-btn__glitch {
  display: block;
}

And that gives us the result we were looking for:

See the Pen CSS Cyberpunk 2077 Button: First pass by SitePoint (@SitePoint) on CodePen.

Wrap Up

And that’s how you recreate the Cyberpunk 2077 buttons with only CSS!

Remember how we used variables for the colors? There was a reason for that. Combining HSL with the variables, we can not only add color variants easily, but we can also add an :active color change too.

Here’s the final working demo.

See the Pen Pure CSS Cyberpunk 2077 Buttons 😎 by SitePoint (@SitePoint) on CodePen.

As always, any questions or suggestions, don’t hesitate to ping me over on Twitter — @jh3yy.

Frequently Asked Questions (FAQs) about Cyberpunk 2077 Button Glitch Effect in CSS

What is the Cyberpunk 2077 Button Glitch Effect in CSS?

The Cyberpunk 2077 Button Glitch Effect in CSS is a unique visual effect that mimics the glitchy, futuristic aesthetic of the popular video game Cyberpunk 2077. This effect is achieved using CSS, a style sheet language used for describing the look and formatting of a document written in HTML. The glitch effect can be applied to buttons on a webpage, giving them a dynamic, interactive feel that enhances user experience.

How can I create a Cyberpunk 2077 Button Glitch Effect in CSS?

Creating a Cyberpunk 2077 Button Glitch Effect in CSS involves several steps. First, you need to define the HTML structure for your button. Then, you apply CSS styles to create the glitch effect. This includes setting the background color, adding a hover effect, and creating the glitch animation using keyframes. You can adjust the properties of the animation to customize the intensity and frequency of the glitch effect.

Can I customize the Cyberpunk 2077 Button Glitch Effect?

Yes, you can customize the Cyberpunk 2077 Button Glitch Effect to suit your needs. You can adjust the color, size, and shape of the button, as well as the intensity and frequency of the glitch effect. This is done by modifying the CSS properties associated with the button and the glitch animation.

Why is my Cyberpunk 2077 Button Glitch Effect not working?

If your Cyberpunk 2077 Button Glitch Effect is not working, there could be several reasons. You may have made a mistake in your HTML structure or CSS code. Check your code for any errors or missing elements. Also, make sure that your browser supports the CSS features you are using. Some older browsers may not support certain CSS properties or animations.

How can I fix the screen glitch effect in Cyberpunk 2077?

The screen glitch effect in Cyberpunk 2077 is a common issue that players encounter. This is not something that can be fixed with CSS, as it is a problem with the game itself. However, there are several solutions available online, including adjusting the game’s graphics settings, updating your graphics drivers, or reinstalling the game.

Can I use the Cyberpunk 2077 Button Glitch Effect on other elements?

Yes, the Cyberpunk 2077 Button Glitch Effect can be applied to other HTML elements, not just buttons. You can apply it to text, images, or any other element on your webpage. The process is the same, you just need to adjust the CSS code to target the desired element.

Is the Cyberpunk 2077 Button Glitch Effect compatible with all browsers?

The Cyberpunk 2077 Button Glitch Effect uses CSS animations, which are supported by all modern browsers. However, some older browsers may not support these features. It’s always a good idea to test your webpage in different browsers to ensure compatibility.

Can I use the Cyberpunk 2077 Button Glitch Effect in a commercial project?

Yes, you can use the Cyberpunk 2077 Button Glitch Effect in a commercial project. However, it’s important to note that the Cyberpunk 2077 brand is copyrighted, so you should avoid using this effect in a way that infringes on that copyright.

How can I make the Cyberpunk 2077 Button Glitch Effect more intense?

You can make the Cyberpunk 2077 Button Glitch Effect more intense by adjusting the properties of the CSS animation. You can increase the frequency of the glitch effect, or make the glitches more pronounced. This can be done by modifying the keyframes of the animation in your CSS code.

Can I combine the Cyberpunk 2077 Button Glitch Effect with other CSS effects?

Yes, you can combine the Cyberpunk 2077 Button Glitch Effect with other CSS effects to create unique and interesting designs. For example, you could add a hover effect, a transition effect, or even another animation. Just make sure to properly organize your CSS code to avoid conflicts between different effects.