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.
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.
And here’s how you can do it!
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.
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: 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.
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
.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.
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.
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
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
<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.
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.
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);
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.
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
.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!
Recreate the Cyberpunk 2077 Button Glitch Effect in CSS