There are a lot of articles out there on making gradient-bordered buttons, but the ones I read do not provide a production-grade solution. At Preply, we needed to have a partially transparent background button with rounded corners, and with gradient border. We need to get it in our Design System, to think about a React Native solution. I know this article is long, but it includes the links to all the resources I read, the solutions I discarded, a link to the Figma file containing the gradient border, and a Codepen where you can play with it.
If you follow me: this article is quite different compared to the high-level, senior-oriented articles I usually write (here is the full list of them) 🙏 but I needed to write it down to be sure it helps other developers in the future.
I had to implement a new variant of the Preply button for our internal Design System: a semi-transparent button with a gradient-rounded border.
The existing solutions
There are some solutions out there but we needed to discard most of them because they do not match the following requirements:
- The button must be transparent: this prevents us from using the most common solution presented in the How To Make a Button with a Gradient Border and Gradient Text in HTML & CSS post. The solution presented here relies on two elements, with a pseudo-element that has the same background color as the page background covering the real element that has a gradient background.
- The button must be half transparent: the solution presented on StackOverflow's Button gradient borders with transparent background does not work since it gives a false idea of transparency and fits just one possible background.
- It must have wider browser support: using engine-only CSS properties like
-webkit-mask
as suggested in Border with gradient and radius does not work well for us. - It must have rounded corners: this discards using CSS
border-image
(here is a good CSS Tricks article about it) since it's not compatible withborder-radius
.
More:
- it should have a fallback for the browsers that do not support the gradient border.
- the border should not be a raster image to avoid pixelation.
- It should be supported by React Native too: more on this topic will follow, forget about it for now.
The viable solution
The last mentioned one, border-image
, in reality, meets all the requirements if mixed with a couple of tricks:
- We can use an SVG instead or a raster image: pixelation is not a problem anymore.
- It's widely supported: at the time of writing, Can I use reports 96.62% (95.43% if unprefixed) browser usage compatibility.
- Through
border-image-slice: stretch
we can define the portions that should scale when the button gets bigger (the sides) and the ones that should not (the corners). - We can use an SVG that includes rounded corners to simulate the final rounded corners.
- We can work around the few bugs reported by Can I use easily (we simply need to use the shorthand syntax) and they do not impact
border-image-slice: stretch
.
Step-by-step implementation
Step 1: Creating the SVG in Figma. This is the Figma file containing it.
Essentially, it's a 100x100 rounded square with the desired border. I made it 100x100 to avoid any kind of coordinates-related problem mentioned in the CSS Tricks article.
Step 2: Optimize the SVG. I was looking for the best way to embed the SVG (external or embedded image?) and the CSS Tricks article points to the great Probably Don’t Base64 SVG that, in turn, points to Optimizing SVGs in data URIs. You can leverage the findings from the latter through mini-svg-data-uri. The steps are
- Reducing the SVG size: easily doable through SVGOMG
- Passing the result through mini-svg-data-uri:
$ npx mini-svg-data-uri image.svg image.svg.uri
- Copy-pasting the
image.svg.uri
content
The result is
border-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none'%3e%3cpath stroke='url(%23a)' stroke-width='2' d='M8 1h84a7 7 0 0 1 7 7v84a7 7 0 0 1-7 7H8a7 7 0 0 1-7-7V8a7 7 0 0 1 7-7Z'/%3e%3cdefs%3e%3clinearGradient id='a' x1='20.27' x2='120.715' y1='125' y2='113.589' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='%23FF7AAC'/%3e%3cstop offset='.367' stop-color='%23FF7AAC'/%3e%3cstop offset='.682' stop-color='%232885FD'/%3e%3cstop offset='1' stop-color='%233DDABE'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")
Choosing the inline or hosted one leads to a different UX, obviously. When I was playing with the hosted one, I hated that the border came with a little delay because of the time required to load the SVG remotely...
Please note that using mini-svg-data-uri
is mandatory, otherwise, the browser does not recognize the SVG. Alternatively, this problem does not happen if you convert the SVG to Base64.
Step 3: Choosing the right border-image-slice
and border-image-width
values. If you need to do something more complex than the case reported in this guide, please refer to the official documentation (1, and 2) and again to the great CSS Tricks article. But for the sake of this guide, the value for either of them is just 8
. Why? Because 8
is the border radius of the button.
If you imagine the border width acting as a sort of "mask" (I highlighted it in yellow in the next images) it must be 8
to be sure it includes the rounder corner. If, instead you set 2
as the real border width, the result is that the "mask" cuts out the rounded corners.
8px | 2px |
---|---|
here you can see the final result of using 8
(correct) and 2
(incorrect) as rendered by the browser
So, the code is now
border-image: url("data:image/svg+xml,...") 8 / 8px;
p.s. if you set the border-width
to 8
instead of border-image-width
, you obtain the same visual effect... but the border is effectively 8 pixels, not 2 pixels like I set for the button! Keep it in mind if there is something wrong and you do not understand why.
Step 4: Choosing the correct border-image-repeat
property. It's stretch
, as I told you earlier. Essentially, it means "Don't touch the corners but extend the sides to be sure the image covers all the size of the button".
border-image: url("data:image/svg+xml,...") 8 / 8px stretch
Step 5: Enjoy the full code 😊
border-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='none'%3e%3cpath stroke='url(%23a)' stroke-width='2' d='M8 1h84a7 7 0 0 1 7 7v84a7 7 0 0 1-7 7H8a7 7 0 0 1-7-7V8a7 7 0 0 1 7-7Z'/%3e%3cdefs%3e%3clinearGradient id='a' x1='20.27' x2='120.715' y1='125' y2='113.589' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='%23FF7AAC'/%3e%3cstop offset='.367' stop-color='%23FF7AAC'/%3e%3cstop offset='.682' stop-color='%232885FD'/%3e%3cstop offset='1' stop-color='%233DDABE'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e") 8 / 8px stretch;
Browser compatibility
At Preply, we must support a wide range of browsers. By looking at the Can I use table I shared above, it's hard to find a non-supported browser and I can be sure the gradient works everywhere. Anyway, I wanted to double-check it with my eyes and I initially used BrowserStack screenshots to test out the solution quickly on a lot of browsers, but it resulted in a bit of unreliable since it was not able to render most of the screenshots due to internal BrowserStack problems... Anyway, I'm happy because when it was able to render the normal button, it was also able to render the gradient one. Here are some of the screenshots on some "not-so-common" browsers
Samsung Galaxy S20 | MacOS / Safari 13 | Windows 10 / Edge 18 |
---|---|---|
Fallback
There are for sure some browsers out there that do not support border-image
. There are two options here:
- Using CSS Feature Queries (
@supports
) ```css
@supports (border-image: url("data:image/svg+xml,...") 8 / 8px stretch;) {
border-image: url("data:image/svg+xml,...") 8 / 8px stretch;
}
This is the solution that gives you more freedom since you can literally change all the button properties you want outside and inside the `@supports` block, you are not limited to changing just `border-image`. Please check the [Feature Queries browser compatibility](https://caniuse.com/css-featurequeries), even if they are almost the same as the one of `border-image`.
2. Simply choose a fallback `border-color`. `border-image` overwrites the `border-color`, but the browsers not able to interpret `border-image` will use `border-color`. Here is an example of Internet Explorer 10.
![Internet Explorer 10 screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ssffpssl0nx2wihmgfg9.jpg)
I opted for the second option because the LESS + PostCSS transpilation process breaks the `@supports` property and I have not found a solution yet.
## React Native
`border-image` does not work with React Native (which simply renders the normal border color instead) and the app developers pointed me to some different solutions.
1. If the button has a gradient border but does not have a transparent background: they would use a combination of [react-native-linear-gradient](https://github.com/react-native-linear-gradient/react-native-linear-gradient) with an element with an opaque background over it.
2. If the button must also be transparent, also [masked-view](https://www.npmjs.com/package/@react-native-masked-view/masked-view) would be needed.
I reported both cases because adding two dependencies for just one button could be overkill. When the button is added to the app, the app developers will decide what to do based on where the button is used. They will maybe opt for implementing the button in the app codebase instead of the Design System one, to avoid putting too many dependencies on the Design System codebase.
But the button is not used in the app at the time of writing so we have the time to go back to the designers asking what they prefer to do.
## Additional notes
1. Don't forget about `background: transparent;` to make the button transparent, of course 😊.
2. Our Design System System's button already sets it but if the button has a background that's not totally transparent, remember to set the `border-radius` to `8px` too to avoid the background coming out of the rounded borders.
## Codepen liknk
You can play with the final result in [this Codepen](https://codepen.io/NoriSte/pen/LYMePqQ) 😊.
## Credit where credit is due
Thank you so much to all the creators sharing their solutions and tools! I would have spent maybe ten times the time on this without their explorations! ❤️
Top comments (0)