How To Make a Button with a Gradient Border and Gradient Text in HTML & CSS

Christian Bolus
9 min readJan 10, 2022

--

A sharply designed button is a great way to invite user engagement. In this tutorial, I’m going to show you how I made the button shown above with a gradient border and gradient text. I should point out that the following approach will only work against a solid background. Throughout the article I’ll be linking to resources that offer a more in-depth explanation of some of the techniques we’ll be using.

If you’d rather skip the explanation and jump straight into the code, you can find an example here.

In order to create this effect, we’re going to stack several elements on top of each other. We’ll accomplish this using a button element with ::before and ::after pseudo-elements. ::before and ::after create child elements of the element that they are attached to.

For a more in-depth explanation of ::before and ::after, I recommend this video.

Our starting HTML and CSS looks like this.

HTML

<div class="container">
<button class="btn"></button>
</div>

CSS

.container {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
.btn {}

The container div is strictly for the purpose of displaying the button in the center of the screen. It is not required if you choose to use this in your own project.

At the moment our button should look like this:

Not very exciting. And as I’m sure you’ve noticed, our button has no text. We’ll come back to that.

The first thing that we’re going to do is create our gradient border effect. This isn’t as straight forward as it might seem. Unfortunately, there’s no easy way to add a gradient to the border property in CSS. No worries, we can make it ourselves. This is where our first pseudo element comes in to help. We’re going to add the following CSS to our .btn class:

.btn {
padding: 20px 60px;
border: none;
outline: none;
position: relative;
border-radius: 5px;
background: linear-gradient(to right, #00FFA3, #DC1FFF);
cursor: pointer;
}

The position: relative property is going to help us place our first pseudo element. Our button should now look like this:

To create the border effect, we’re going to place the ::before pseudo-element inside of our button and give it a background-color of whichever color the background of our page is (in our case white). We’ll add the following CSS below our .btn class:

.btn::before {
content: "";
position: absolute;
top: 1px;
right: 1px;
bottom: 1px;
left: 1px;
background-color: white;
}

Our button should now look like this:

Not exactly what we want but we’ll fix it. Let’s break down what’s going on here. The ::before addition creates a pseudo-element that is a child of our button. The position: absolute property is used to remove an element from the flow of a document. If the element placed with position: absolute does not have an ancestor with a position property, it will be placed relative to the root element (the html tag). However, since we added position: relative to our button, and ::before is a child of our button, it will be placed relative to the button.

For a more in-depth explanation of position: absolute and
position: relative, I recommend this video.

The top, right, bottom and left properties pull ::before's edges 1px away from the edges of our button. To understand why there are gaps in the corners of our button, let’s change the background-color of ::before to black:

.btn::before {
content: "";
position: absolute;
top: 1px;
right: 1px;
bottom: 1px;
left: 1px;
background-color: black;
}

If you look closely, you can see that the corners of ::before are square and are therefore cutting into the rounded corners of our button. This gives the appearance of a gap when ::before is the same color as our background. To fix this we’ll add some border-radius to ::before. I like to use to following formula to calculate how much border-radius to give to a nested element so that we have an even gap between the two elements:

Using this formula with the values we already have will give us the correct border-radius for ::before. Our button has a border-radius: 5px (R1) and we have a 1px gap (D) between our button and ::before. That means ::before should have a border-radius: 4px.
R1 — D = R2 -> 5 — 1 = 4:

.btn::before {
content: "";
position: absolute;
top: 1px;
right: 1px;
bottom: 1px;
left: 1px;
background-color: black;
border-radius: 4px;
}

Now that we have our borders properly radiused, we can change ::before’s background-color back to white:

.btn::before {
content: "";
position: absolute;
top: 1px;
right: 1px;
bottom: 1px;
left: 1px;
background-color: white;
border-radius: 4px;
}

Our gradient border effect is now complete. Now onto the gradient text.

Because ::before is a child of our button and has position: absolute, ::before is stacked on top of our button. This means that we can’t use the text that we would normally place between the button tags because it will be covered by ::before. To solve this problem, we’re going to use another pseudo-element, ::after. We’re also going to add a data attribute to our button element.

The HTML looks like this:

<div class="container">
<button class="btn" data="Click me"></button>
</div>

And the CSS for ::after looks like this:

.btn::after {
content: attr(data);
font-size: 16px;
}

Let’s break this down. attr() is a CSS function that returns the value of an attribute (in our case the data attribute). We’re using it to set the value of the content property which will be “Click me”. You could hard-code the content property, but using the attr() function allows the button to be configurable in case you are using this as a reusable component in a JavaScript library/framework such as React:

You will most likely have noticed that we don’t see any text. However, our button is now larger which means that the padding that we added at the beginning to our .btn class is pushing against something. We don’t see the text because ::before (because of its position: absolute) is currently covering ::after as well. Let’s comment out ::before for the time being:

/* .btn::before {
content: "";
position: absolute;
left: 1px;
right: 1px;
top: 1px;
bottom: 1px;
border-radius: 4px;
background-color: #fff;
z-index: -1;
} */

You should now see this:

The first thing we need to do to create the gradient text effect is give the background of ::after a linear-gradient using the same colors as our button but in the opposite direction:

.btn::after {
content: attr(data);
font-size: 16px;
background: linear-gradient(to left, #00FFA3, #DC1FFF);
}

That should give us this:

The next step is to add -webkit-background-clip: text and color: transparent to ::after. We use the -webkit vendor prefix on background-clip as this property isn’t fully supported across all browsers yet:

.btn::after {
content: attr(data);
font-size: 16px;
background: linear-gradient(to left, #00FFA3, #DC1FFF);
-webkit-background-clip: text;
color: transparent;
}

The background-clip property determines how far an element’s background extends beyond its content. By setting the value to text, we’re basically saying that we want the gradient background that we set on ::after to only be visible behind the space occupied by the characters of the text itself. Setting color: transparent allows the clipped background to show through the text, giving us the gradient text effect.

For more information on the background-clip property I recommend this article.

Let’s comment back in our ::before CSS:

.btn::before {
content: "";
position: absolute;
left: 1px;
right: 1px;
top: 1px;
bottom: 1px;
border-radius: 4px;
background-color: white;
}

To complete our button we need to stack our elements in the correct order. Because our button element has a position property applied, we can create a new stacking context by adding z-index: 1 to our .btn class:

.btn {
padding: 20px 60px;
border: none;
outline: none;
position: relative;
border-radius: 5px;
background: linear-gradient(to right, #00FFA3, #DC1FFF);
cursor: pointer;
z-index: 1;
}

You won’t see any change just yet:

The z-index property represents the stacking order of an element along the z-axis. Elements with a higher value will appear on top. So in order to move ::before behind ::after, we’ll give it z-index: -1. You might think that this would move ::before behind our button as well since -1 is less than 1. However, since ::before and ::after are children of our button element and our button creates a new stacking context, ::before and ::after will always be stacked on top of our button. Therefore, giving ::before a z-index: -1 moves it behind ::after while keeping it on top of our button.

For more information on stacking contexts and z-index I recommend this article and this video:

.btn::before {
content: "";
position: absolute;
left: 1px;
right: 1px;
top: 1px;
bottom: 1px;
border-radius: 4px;
background-color: white;
z-index: -1;
}

Our button is now complete:

…but let’s add a few finishing touches.

First we’re going to add a transition: 200ms to ::before and ::after.

.btn::before {
content: "";
position: absolute;
left: 1px;
right: 1px;
top: 1px;
bottom: 1px;
border-radius: 4px;
background-color: white;
z-index: -1;
transition: 200ms
}
.btn::after {
content: attr(data);
font-size: 16px;
background: linear-gradient(to left, #00FFA3, #DC1FFF);
-webkit-background-clip: text;
color: transparent;
transition: 200ms
}

Next we’re going to use the :hover pseudo-class on ::before and ::after to add a hover effect to our button:

.btn:hover::before {
opacity: 50%;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.btn:hover::after{
color: white;
}

The top, right, bottom and left properties on the :hover pseudo-class on ::before will extend it to be the full width and height of our button. Combined with the opacity: 50% and color: white on ::after we get the following effect when hovering over the button:

I hope that you have found this tutorial helpful. Thanks for reading and if you have any suggestions for improvements please let me know. Happy coding!

The entire HTML and CSS can be found below and an example CodePen can be found here:

HTML

<div class="container">
<button class="btn" data="Click me"></button>
</div>

CSS

.container {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
}
.btn {
padding: 20px 60px;
border: none;
outline: none;
position: relative;
z-index: 1;
border-radius: 5px;
background: linear-gradient(to right, #00FFA3, #DC1FFF);
cursor: pointer;
}
.btn::before {
content: "";
position: absolute;
left: 1px;
right: 1px;
top: 1px;
bottom: 1px;
border-radius: 4px;
background-color: white;
z-index: -1;
transition: 200ms
}
.btn::after {
content: attr(data);
font-size: 16px;
background: linear-gradient(to left, #00FFA3, #DC1FFF);
-webkit-background-clip: text;
color: transparent;
transition: 200ms
}
.btn:hover::before {
opacity: 50%;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.btn:hover::after{
color: white;
}

--

--