The Complex But Awesome CSS border-image Property

The Complex But Awesome CSS border-image Property

The border-image property is nothing new. Even deprecated Internet Explorer supports it, so you know we’re treading well-charted territory. At the same time, it’s not exactly one of those properties you likely keep close at hand, and its confusing concepts of “slicing” and “outsets” don’t make it the easiest property to use.

I’m here to tell you that border-image is not only capable of producing some incredibly eye-catching UI designs but is also able to accomplish some other common patterns we often turn to other properties and approaches for.

In this article, my plan is to dispel the confusing aspects of border-image without getting into a bunch of theoretical explanations and technical jargon. Instead, we’re going to have fun with the property, using it to create shapes and put a different spin on things like range sliders, tooltips, and title decorations.

By the end of this, I hope that border-image becomes your new favorite property just as it has become mine!

The Concept of Image Borders

There are a few specific aspects of border-image that I think are crucial for understanding how it works.

It’s Easy To Accidentally Override A Border Image

The CSS Backgrounds and Border Module Level 3 specification says border-image should replace any regular border you define, but it’s not always the case. Try the code below, and all you will see is a red border.

/* All I see is a red border */
.element {
  border-image: linear-gradient(blue, red) 1;
  border: 5px solid red;
}

That’s because we’re technically declaring border after border-image. Order really does matter when working with border-image!

/* 👍 */
.element {
  border: 5px solid red;
  border-image: linear-gradient(blue, red) 1;
}

You can already see how this could be confusing for anyone jumping into border-image, especially for the first time. You will also notice that our gradient border has a thickness equal to 5px, which is the border-width we defined.

I make it a personal habit not to use border and border-image together because it helps me avoid overriding the border image I’m trying to create and be able to control the border decoration using only one property (even if both can be used together). So, if you get a strange result, don’t forget to check if you have a border declared somewhere.

It Is Painted Over Backgrounds And Box Shadows

The second tip I want to offer is that border-image is painted above the element’s background and box-shadow but below the element’s content. This detail is important for some of the tricks we will use in later examples. The following Pen demonstrates how a border image is applied in that order:

If we were to translate the figure above into code using the provided variables as values, it would look like this:

border-image:
  linear-gradient(...)
  s-top s-right s-bottom s-left / 
  w-top w-right w-bottom w-left /
  o-top o-right o-bottom o-left;

By default, border-image considers the boundaries of the element (illustrated with the blue dotted border in Figure 1) as its area to paint the gradient, but we can change this using the <outset> to increase that area and create an overflow. This is super useful to have “outside” decorations.

Then, the <width> is used to split the area into nine regions, and the <slice> is used to split the source (i.e., the gradient) into nine slices as well. From there, we assign each slice to its corresponding region. Each slice is stretched to fill its assigned region, and if they don’t share equal dimensions, the result is typically a distorted image slice. Later on, we will learn how to control that and prevent distortion.

The middle region is kept empty by default. That said, it is totally possible to use the fill keyword to do what it says and fill the middle region with slice nine (which is always the center slice).

border-image: linear-gradient(...) fill
  s-top s-right s-bottom s-left / 
  w-top w-right w-bottom w-left /
  o-top o-right o-bottom o-left;

I know this was a pretty fast primer on border-image, but I think it’s all we need to do some pretty awesome stuff. Let’s jump into the fun and start experimenting with effects.

Gradient Overlay

Our first experiment is to add a gradient overlay above an existing background. This is a fairly common pattern to improve the legibility of text by increasing the contrast between the text color and the background color.

There are several well-known approaches to setting an overlay between text and content. Here’s one from Chris Coyier back in 2013. And that isn’t even the most widely-used approach, which is likely using pseudo-elements.

But border-image gives us a one-line way to pull it off:

.overlay {
  border-image: fill 0 linear-gradient(#0003,#000); 
}

That’s all! No extra element, no pseudo-element, and no need to modify the background property.

Well, guess what? The border-image property can pull it off with one line of code:

.full-background {
  border-image: conic-gradient(pink 0 0) fill 0//0 100vw;
}

If you compare what we just did with the gradient overlay example, the <outset> is the only difference between the implementations. Other than that, we have a single slice placed in the middle region that covers the entire area we extended to the edge of the screen.

We are not limited to a solid color, of course, since we are working with gradients.

Fancy Headings

Another thing we can use border-image for is decorating headings with fancy borders. Let’s start with the exact same implementation we used for the full-width backgrounds. Only this time, we’re replacing the conic-gradient() with a linear-gradient():

.full-background {
  border-image: linear-gradient(0deg, #1095c1 5px, lightblue 0) fill 0//0 100vw;
}

Now we apply this to an <h1> element:

So, that’s two different ways to get the same effect using the same border-image syntax. We can actually get this a third way as well:

.full-line {
  border-image: conic-gradient(#1095c1 0 0) 0 0 1 0/0 0 8px 0/0 100vw 0 0;
}

This time, I have defined a bottom slice equal to 1 (unitless values are computed as pixels), which produces two slices, the seventh (bottom center) and the ninth (center). From there, I have set the seventh region to a height of 8px. Note that I am not using the fill keyword this time, so the middle region is not filled like it was last time. Instead, we only fill the seventh region that takes up 100% of the boder-image’s area and 8px of its height.

You’re wondering why I am defining a slice equal to 1, right? The goal is to have only two slices: the seventh (bottom center) and the ninth (middle), and since we are applying a solid color, the size doesn't matter. That’s why I used 1; a small positive value. Any value will work (e.g., 0.5, 2, 100, 50%, 76%, and so on); it’s just that 1 is shorter to type. Remember that the slice will get stretched within its region, so 1px is enough to fill the whole region.

Here’s the deal: The slice value doesn’t really matter when working with a solid coloration. In most cases, the value winds up being 0 (empty) or 1 (filled). You can think of it as binary logic.

We could do this a fourth way!

.full-line {
  border-image: conic-gradient(#1095c1 0 0) 0 1 0 0/calc(100% - 8px) 100% 0 0/0 100vw 0 0;
}

I’ll let you do the work to figure out how the above CSS works. It’s a good opportunity to get a feel for slicing elements. Take a pen and paper and try to identify which slices we are using and which regions will be filled.

One thing that makes border-image a complex property is all the different ways to achieve the same result. You can wind up with a lot of different combinations, and when all of them produce the same result, it’s tough to form a mental model for understanding how all of the values work together.

Even though there is no single “right” way to do these heading borders, I prefer the second syntax because it allows me to simply change one color value to establish a “real” gradient instead of a solid color.

.full-line {
  border-image: repeating-linear-gradient(...) fill 0 /
    calc(100% - var(--b)) 0 0/0 100vw 0 0 repeat;
}

Let’s try another syntax for the same effect:

h2 {
  --s: 3px;   /* the thickness */
  --w: 100px; /* the width */
  --g: 10px;  /* the gap */
  border-image: 
     conic-gradient(red 0 0) 
     0 50%/calc(50% - var(--s)/2) var(--w)/0 calc(var(--w) + var(--g));
}

The top and bottom values of the <slice> are equal to 0, and the left and right ones are equal to 50%. This means that slices six and eight share the gradient. All the other slices — including the center — are empty.

As far as the regions go, the top and bottom regions (consisting of regions 1, 5, and 2 at the top and regions 4, 7, and 3 at the bottom) have a height equal to 50% - var(--s)/2 leaving the --s variable as a height for the remaining regions (6, 8, and 9). The right and the left regions have a width equal to the --w variable. Since slices 6 and 8 are the only ones that are filled, the only regions we need to care about are 6 and 8. Both have a height equal to the border’s thickness, --s, and a width equal to --w.

I think you know how the rest of the story goes.

Notice I am using 50% as a slice. It demonstrated how any value does the job, as we discussed in the last section when I explained why I chose to use a value of 1 but also to prepare for the next effect where I will be using a real gradient:

See the Pen Horizontal lines around your title with gradient coloration by Temani Afif.

When it comes to real gradients, the value of the slice is important, and sometimes you need very precise values. To be honest, this can be very tricky, and I even get lost trying to figure out the right value.

Let’s end this section with more examples of title decorations. When combined with other properties, border-image can make really nice effects.

See the Pen Fancy title divider with one element by Temani Afif.

See the Pen Fancy title divider with one element by Temani Afif.

More Examples

Now that we’ve seen several detailed examples of how border-image, I’m going to drop in several other examples. Rather than explaining them in great detail, try to explain them in your own words by inspecting the CSS, and use these as inspiration for your own work.

Infinite Image Decorations

When it comes to images, border-image can be a lifesaver since we don’t have access to pseudo-elements. Here are some cool infinite decorations where we can have a touch of 3D effect.

See the Pen Infinite image shadow by Temani Afif.

See the Pen Infinite image shadow II by Temani Afif.

See the Pen Infinite image stripes shadow by Temani Afif.

See the Pen 3D trailing shadow for images by Temani Afif.

If you check the code in these examples, you will find they share nearly the same structure. If you have trouble recognizing the pattern, please don’t hesitate to leave a comment at the end of this article, and I would be happy to point it out.

Custom Range Slider

I wrote a detailed article on how to create the following example, and you can refer to it for range slider variations using the same technique.

See the Pen CSS only custom range sliders by Temani Afif.

I used border-image and styled only the “thumb” element. Range inputs are known to have different implementation cross-browser, but the “thumb” is common between all of them.

Ribbon Shapes

In case you missed it, I have created a collection of more than 100 single-element ribbon shapes, and some of them rely on border-image. I call them the “infinite ribbons.”

See the Pen Full screen Ribbon title by Temani Afif.

See the Pen Infinite Ribbon Shapes by Temani Afif.

Heart Shapes

I have written about CSS heart shapes using different approaches, and one of them uses a border-image technique.

.heart {
  width: 200px;
  aspect-ratio: 1;
  border-image: radial-gradient(red 69%,#0000 70%) 84.5%/50%;
  clip-path: polygon(-42% 0,50% 91%, 142% 0);
}

See the Pen Heart shape using border-image by Temani Afif.

The interesting part here is the slice that is equal to 84.5%. That is a bigger value than 50%, so it may seem incorrect since the total exceeds 100%. But it’s perfectly fine because slices are able to overlap one another!

When using values bigger than 50%, the corner slices (1, 2, 3, and 4) will share common parts, but the other slices are considered empty. Logically, when using a slice equal to 100%, we will end with four slices containing the full source.

Here is an example to illustrate the trick:

See the Pen Overview of the slice effect by Temani Afif.

The slider will update the slice from 0% to 100%. On the left, you can see how the corner slices (1-4) grow. Between 0% and 50%, the result is logical and intuitive. Bigger than 50%, you start having the overlap. When reaching 100%, you can see the full circle repeated four times because each slice contains the full gradient, thanks to the overlap.

It can be confusing and not easy to visualize, but overlaps can be really useful to create custom shapes and fancy decorations.

Tooltips

What about a simple tooltip shape with only two properties? Yes, it’s possible!

See the Pen A simple Tooltip using 2 CSS properties by Temani Afif.

.tooltip {
  /* triangle dimension */
  --b: 2em; /* base */
  --h: 1em; /* height*/

  border-image: conic-gradient(#CC333F 0 0) fill 0//var(--h);
  clip-path: 
    polygon(0 100%,0 0,100% 0,100% 100%,
      calc(50% + var(--b)/2) 100%,
      50% calc(100% + var(--h)),
      calc(50% - var(--b)/2) 100%);
}
Filling Border Radius

Unlike most decorative border properties (e.g., box-shadow, outline, border, and so on), border-image doesn’t respect border-radius. The element is still a box, even if we’ve rounded off the corners. Other properties will recognize the visual boundary established by border-radius, but border-image bleeds right through it.

That could be a drawback in some instances, I suppose, but it’s also one of the quirky things about CSS that can be leveraged for other uses like creating images with inner radius:

See the Pen Inner radius to image element by Temani Afif.

Cool, right? Only one line of code makes it happen:

img {
  --c: #A7DBD8;
  --s: 10px; /* the border thickness*/

  border-image: conic-gradient(var(--c) 0 0) fill 0 // var(--s);
}

We can even leave the center empty to get a variation that simply borders the entire element:

See the Pen Rounded images inside squares by Michelle Barker.

Conclusion

Did you know border-image property was such a powerful — and flexible — CSS property? Despite the challenge it takes to understand the syntax, there are ways to keep the code clean and simple. Plus, there is often more than one “right” way to get the same result. It’s a complicated and robust CSS feature.

If the concepts of slicing and defining regions with border-image are still giving you problems, don’t worry. That’s super common. It took me a lot of time to fully understand how border-image works and how to use it with different approaches to the syntax. Give yourself plenty of time, too. It helps to re-read things like this article more than once to let the concepts sink in.

Complexities aside, I hope that you will add border-image to your toolbox and create a lot of magic with it. We can do even more with border-image than what was demonstrated here. I actually experiment with this sort of stuff frequently and share my work over at my CSS Tip website. Consider subscribing (RSS) to keep up with the fun and weird things I try.

Special thanks to @SelenIT2, who pushed me to explore this property and wrote an excellent article on it.