How To Make a Button with a Gradient Border and Gradient Text in HTML & CSS
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;
}