border-image – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 30 Sep 2022 17:54:41 +0000 en-US hourly 1 https://wordpress.org/?v=6.2.2 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 border-image – CSS-Tricks https://css-tricks.com 32 32 45537868 Flip, Invert, and Reverse https://css-tricks.com/flip-invert-and-reverse/ https://css-tricks.com/flip-invert-and-reverse/#respond Thu, 09 Dec 2021 00:27:11 +0000 https://css-tricks.com/?p=358554 The SVG <path> syntax is a beast. There are all sorts of commands that make up a mini-language all of its own — so powerful that it’s capable of drawing anything. Don’t be too scared of it, though, because some …


Flip, Invert, and Reverse originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The SVG <path> syntax is a beast. There are all sorts of commands that make up a mini-language all of its own — so powerful that it’s capable of drawing anything. Don’t be too scared of it, though, because some of the commands are very straightforward to understand. They read like “put pen here, draw 2 over, draw 3 down, draw 1 up, done.” Yuan Chuan starts there and demonstrates:

If you take that design and, as the title of the article suggests, flip, invert, and reverse it, you can build some really neat patterns.

That syntax you see is part of CSS Doodle and you can see the result here:


If that type of aesthetic appeals to you, it might be because you used to doodle this way as a kid, like Clive Thompson.

When I was in grade school, I used to have a very particular style of doodling.

I’d draw a pattern that was governed by five rules, rather like the ones governing an Etch-A-Sketch. They were:

1. You draw one single line. It can be as long as you like.
2. To start the line, you put your pen down.
3. You can make right-angle turns only, either 90 degrees or -90 degrees.
4. You cannot back up. You must always move forward.
5. You don’t lift your pen until you’re ready to stop. When you lift the pen, the doodle is done.

He made a Right-Angle Doodling Machine because, of course, as adults we can’t just be nostalgic — we have to throw technology and time at it to try to re-summon whatever made the thing feel special originally. But it’s never quite the same:

Does doodling with an app tickle our brains in the same way?

I don’t think so. When I mess around with this right-angle drawing-machine, my brain doesn’t feel quite as activated, nor as rested, as when I make the same drawings by hand.

To Shared LinkPermalink on CSS-Tricks


Flip, Invert, and Reverse originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/flip-invert-and-reverse/feed/ 0 358554
CSS-ing Candy Ghost Buttons https://css-tricks.com/css-ing-candy-ghost-buttons/ https://css-tricks.com/css-ing-candy-ghost-buttons/#comments Mon, 01 Nov 2021 01:04:53 +0000 https://css-tricks.com/?p=354804 Recently, while looking for some ideas on what to code as I have zero artistic sense so the only thing I can do is find pretty things that other people have come up with and remake them with clean and …


CSS-ing Candy Ghost Buttons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Recently, while looking for some ideas on what to code as I have zero artistic sense so the only thing I can do is find pretty things that other people have come up with and remake them with clean and compact code… I came across these candy ghost buttons!

They seemed like the perfect choice for a cool little thing I could quickly code. Less than fifteen minutes later, this was my Chromium result:

Chrome screenshot. Shows a four row, five column grid of candy ghost buttons with text and an icon following it. These buttons have an elongated pill-like shape, a transparent background and a continuous sweet pastel gradient for the border and the text and icon inside.
The pure CSS candy ghost buttons.

I thought the technique was worth sharing, so in this article, we’ll be going through how I first did it and what other options we have.

The starting point

A button is created with… are you ready for this? A button element! This button element has a data-ico attribute in which we drop an emoji. It also has a stop list custom property, --slist, set in the style attribute.

<button data-ico="👻" style="--slist: #ffda5f, #f9376b">boo!</button>

After writing the article, I learned that Safari has a host of problems with clipping to text, namely, it doesn’t work on button elements, or on elements with display: flex (and perhaps grid too?), not to mention the text of an element’s children. Sadly, this means all of the techniques presented here fail in Safari. The only workaround is to apply all the button styles from here on a span element nested inside the button, covering its parent’s border-box. And, in case this helps anyone else who, like me, is on Linux without physical access to an Apple device (unless you count the iPhone 5 someone on the fourth floor — who you don’t want to bother with stuff like this more than twice a month anyway — bought recently), I’ve also learned to use Epiphany in the future. Thanks to Brian for the suggestion!

For the CSS part, we add the icon in an ::after pseudo-element and use a grid layout on the button in order to have nice alignment for both the text and the icon. On the button, we also set a border, a padding, a border-radius, use the stop list, --slist, for a diagonal gradient and prettify the font.

button {
  display: grid;
  grid-auto-flow: column;
  grid-gap: .5em;
  border: solid .25em transparent;
  padding: 1em 1.5em;
  border-radius: 9em;
  background: 
    linear-gradient(to right bottom, var(--slist)) 
      border-box;
  font: 700 1.5em/ 1.25 ubuntu, sans-serif;
  text-transform: uppercase;
  
  &::after { content: attr(data-ico) }
}

There’s one thing to clarify about the code above. On the highlighted line, we set both the background-origin and the background-clip to border-box. background-origin both puts the 0 0 point for background-position in the top-left corner of the box it’s set to and gives us the box whose dimensions the background-size is relative to.

That is, if background-origin is set to padding-box, the 0 0 point for background-position is in the top left-corner of the padding-box. If background-origin is set to border-box, the 0 0 point for background-position is in the top-left corner of the border-box. If background-origin is set to padding-box, a background-size of 50% 25% means 50% of the padding-box width and 25% of the padding-box height. If background-origin is set to border-box, the same background-size of 50% 25% means 50% of the border-box width and 25% of the border-box height.

The default value for background-origin is padding-box, meaning that a default-sized 100% 100% gradient will cover the padding-box and then repeat itself underneath the border (where we cannot see it if the border is fully opaque). However, in our case, the border is fully transparent and we want our gradient to stretch across the entire border-box. This means we need to change the background-origin value to border-box.

Screenshot collage. Chrome on the left, Firefox on the right, showing differences between ghost emojis. The button has a pastel gradient background going along the main diagonal, the text 'Boo!' in black and a ghost emoji, which is going to look different depending on the OS and browser.
The result after applying the base styles (live demo).

The simple, but sadly non-standard Chromium solution

This involves using three mask layers and compositing them. If you need a refresher on mask compositing, you can check out this crash course.

Note that in the case of CSS mask layers, only the alpha channel matters, as every pixel of the masked element gets the alpha of the corresponding mask pixel, while the RGB channels don’t influence the result in any way, so they may be any valid value. Below, you can see the effect of a purple to transparent gradient overlay versus the effect of using the exact same gradient as a mask.

Screenshot. Shows two Halloween-themed cat pictures (the cat is protectively climbed on top of a Halloween pumpkin) side by side. The first one has a purple to transparent linear gradient overlay on top. The second one uses the exact same linear gradient as a mask. By default, CSS masks are alpha masks, meaning that every pixel of the masked element gets the alpha of the corresponding mask pixel.
Gradient overlay vs. the same gradient mask (live demo).

We’re going to start with the bottom two layers. The first one is a fully opaque layer, fully covering the entire border-box, meaning that it has an alpha of 1 absolutely everywhere. The other one is also fully opaque, but restricted (by using mask-clip) to the padding-box, which means that, while this layer has an alpha of 1 all across the padding-box, it’s fully transparent in the border area, having an alpha of 0 there.

If you have a hard time picturing this, a good trick is to think of an element’s layout boxes as nested rectangles, the way they’re illustrated below.

Illustration showing the layout boxes. The outermost box is the border-box. Inside it, a border-width away from the border limit, we have the padding-box. And finally, inside the padding-box, a padding away from the padding limit, we have the content-box.
The layout boxes (live demo).

In our case, the bottom layer is fully opaque (the alpha value is 1) across the entire orange box (the border-box). The second layer, that we place on top of the first one, is fully opaque (the alpha value is 1) all across the red box (the padding-box) and fully transparent (with an alpha of 0) in the area between the padding limit and the border limit.

One really cool thing about the limits of these boxes is that corner rounding is determined by the border-radius (and, in the case of the padding-box, by the border-width as well). This is illustrated by the interactive demo below, where we can see how the corner rounding of the border-box is given by the border-radius value, while the corner rounding of the padding-box is computed as the border-radius minus the border-width (limited at 0 in case the difference is a negative value).

Now let’s get back to our mask layers, one being fully opaque all across the entire border-box, while the one on top of it is fully opaque across the padding-box area and fully transparent in the border area (between the padding limit and the border limit). These two layers get composited using the exclude operation (called xor in the non-standard WebKit version).

Illustration. Shows the bottom two background layers in 3D. The first one from the bottom has an alpha of 1 all across the entire border-box. The second one, layered on top of it, has an alpha of 1 across the padding box, within the padding limit; it also has an alpha of 0 in the border area, outside the padding limit, but inside the border limit.
The two base layers (live demo).

The name of this operation is pretty suggestive in the situation where the alphas of the two layers are either 0 or 1, as they are in our case — the alpha of the first layer is 1 everywhere, while the alpha of the second layer (that we place on top of the first) is 1 inside the padding-box and 0 in the border area between the padding limit and the border limit.

In this situation, it’s pretty intuitive that the rules of boolean logic apply — XOR-ing two identical values gives us 0, while XOR-ing two different ones gives us 1.

All across the padding-box, both the first layer and the second one have an alpha of 1, so compositing them using this operation gives us an alpha of 0 for the resulting layer in this area. However, in the border area (outside the padding limit, but inside the border limit), the first layer has an alpha of 1, while the second one has an alpha of 0, so we get an alpha of 1 for the resulting layer in this area.

This is illustrated by the interactive demo below, where you can switch between viewing the two mask layers separated in 3D and viewing them stacked and composited using this operation.

Putting things into code, we have:

button {
  /* same base styles */
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  mask: var(--full) padding-box exclude, var(--full);
}

Before we move further, let’s discuss a few fine-tuning details about the CSS above.

First off, since the fully opaque layers may be anything (the alpha channel is fixed, always 1 and the RGB channels don’t mater), I usually make them red — only three characters! In the same vein, using a conic gradient instead of a linear one would also save us one character, but I rarely ever do that since we still have mobile browsers that support masking, but not conic gradients. Using a linear one ensures we have support all across the board. Well, save for IE and pre-Chromium Edge but, then again, not much cool shiny stuff works in those anyway.

Second, we’re using gradients for both layers. We’re not using a plain background-color for the bottom one because we cannot set a separate background-clip for the background-color itself. If we were to have the background-image layer clipped to the padding-box, then this background-clip value would also apply to the background-color underneath — it would be clipped to the padding-box too and we’d have no way to make it cover the entire border-box.

Third, we’re not explicitly setting a mask-clip value for the bottom layer since the default for this property is precisely the value we want in this case, border-box.

Fourth, we can include the standard mask-composite (supported by Firefox) in the mask shorthand, but not the non-standard one (supported by WebKit browsers).

And finally, we always set the standard version last so it overrides any non-standard version that may also be supported.

The result of our code so far (still cross-browser at this point) looks like below. We’ve also added a background-image on the root so that it’s obvious we have real transparency across the padding-box area.

Screenshot. The pastel gradient button is just a shadow of its former self. Well, just a border, that's all we can see of it. The entire area inside the padding limit has been masked out and we can now see through to the image background behind the button.
The result after masking out the entire padding-box (live demo).

This is not what we want. While we have a nice gradient border (and by the way, this is my preferred method of getting a gradient border since we have real transparency all across the padding-box and not just a cover), we are now missing the text.

So the next step is to add back the text using yet another mask layer on top of the previous ones, this time one that’s restricted to text (while also making the actual text fully transparent so that we can see the gradient background through it) and XOR this third mask layer with the result of XOR-ing the first two (result that can be seen in the screenshot above).

The interactive demo below allows viewing the three mask layers both separated in 3D as well as stacked and composited.

Note that the text value for mask-clip is non-standard, so, sadly, this only works in Chrome. In Firefox, we just don’t get any masking on the button anymore and having made the text transparent, we don’t even get graceful degradation.

button {
  /* same base styles */
  -webkit-text-fill-color: transparent;
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  /* sadly, still same result as before :( */
  mask: var(--full) padding-box exclude, var(--full);
}

If we don’t want to make our button unusable this way, we should put the code that applies the mask and makes the text transparent in a @supports block.

button {
  /* same base styles */

  @supports (-webkit-mask-clip: text) {
    -webkit-text-fill-color: transparent;
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). In Chrome, we have a real pill-shaped pastel gradient ghost button. It has a transparent background that lets us see through to the image background behind our button and a continuous sweet pastel gradient for the border and the text and icon inside. In Firefox, we have the same pill-shaped, pastel background, black text and normal emoji button we had after setting the base styles. The ghost emoji is going to look different depending on the OS and browser - here it can be seen it has different looks in Chrome and Firefox.
The final result using the masking-only method (live demo).

I really like this method, it’s the simplest we have at this point and I’d really wish text was a standard value for mask-clip and all browsers supported it properly.

However, we also have a few other methods of achieving the candy ghost button effect, and although they’re either more convoluted or more limited than the non-standard Chromium-only one we’ve just discussed, they’re also better supported. So let’s take a look at those.

The extra pseudo-element solution

This involves setting the same initial styles as before, but, instead of using a mask, we clip the background to the text area.

button {
  /* same base styles */
  background: 
    linear-gradient(to right bottom, var(--slist)) 
    border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent
}

Just like before, we need to also make the actual text transparent, so we can see through it to the pastel gradient background behind it that is now clipped to its shape.

Screenshot collage. Chrome (left) vs. Firefox (right), highlighting the differences in emoji shapes when they're part of knockout text. This is entirely normal and fine, as emojis look different depending on OS and browser.
Knockout button text (live demo).

Alright, we have the gradient text, but now we’re missing the gradient border. So we’re going to add it using an absolutely positioned ::before pseudo-element that covers the entire border-box area of the button and inherits the border, border-radius and background from its parent (save for the background-clip, which gets reset to border-box).

$b: .25em;

button {
  /* same as before */
  position: relative;
  border: solid $b transparent;
  
  &::before { 
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: inherit;
    background-clip: border-box;
    content: '';
  }
}

inset: -$b is a shorthand for:

top: -$b;
right: -$b;
bottom: -$b;
left: -$b

Note that we’re using the border-width value ($b) with a minus sign here. The 0 value would make the margin-box of the pseudo (identical to the border-box in this case since we have no margin on the ::before) only cover the padding-box of its button parent and we want it to cover the entire border-box. Also, the positive direction is inwards, but we need to go outwards by a border-width to get from the padding limit to the border limit, hence the minus sign — we’re going in the negative direction.

We’ve also set a negative z-index on this absolutely positioned element since we don’t want it to be on top of the button text and prevent us from selecting it. At this point, text selection is the only way we can distinguish the text from the background, but we’ll soon fix that!

Screenshot. Shows how text selection is the only way of still distinguishing the transparent text and gradient background clipped to text area button from its gradient background ::before pseudo that covers it fully.
The result after adding the gradient pseudo (live demo).

Note that since pseudo-element content isn’t selectable, the selection only includes the button’s actual text content and not the emoji in the ::after pseudo-element as well.

The next step is to add a two layer mask with an exclude compositing operation between them in order to leave just the border area of this pseudo-element visible.

button {
  /* same as before */
    
  &::before { 
    /* same as before */
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
    mask: var(--full) padding-box exclude, var(--full);
  }
}

This is pretty much what we did for the actual button in one of the intermediate stages of the previous method.

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The final result using the extra pseudo method (live demo).

I find this to be the best approach in most cases when we want something cross-browser and that doesn’t include IE or pre-Chromium Edge, none of which ever supported masking.

The border-image solution

At this point, some of you may be screaming at the screen that there’s no need to use the ::before pseudo-element when we could use a gradient border-image to create this sort of a ghost button — it’s a tactic that has worked for over three quarters of a decade!

However, there’s a very big problem with using border-image for pill-shaped buttons: this property doesn’t play nice with border-radius, as it can be seen in the interactive demo below. As soon as we set a border-image on an element with border-radius, we lose the corner rounding of the border, even through, funny enough, the background will still respect this rounding.

Still, this may be a simple solution in the case where don’t need corner rounding or the desired corner rounding is at most the size of the border-width.

In the no corner rounding case, save for dropping the now pointless border-radius, we don’t need to change the initial styles much:

button {
  /* same base styles */
  --img: linear-gradient(to right bottom, var(--slist));
  border: solid .25em;
  border-image: var(--img) 1;
  background: var(--img) border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

The result can be seen below, cross-browser (should be supported supported even in pre-Chromium Edge).

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with no rounded corners. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The no corner rounding result using the border-image method (live demo).

The trick with the desired corner rounding being smaller than the border-width relies on the way border-radius works. When we set this property, the radius we set represents the rounding for the corners of the border-box. The rounding for the corners of the padding-box (which is the inner rounding of the border) is the border-radius minus the border-width if this difference is positive and 0 (no rounding) otherwise. This means we have no inner rounding for the border if the border-radius is smaller than or equal to the border-width.

In this situation, we can use the inset() function as a clip-path value since it also offers the possibility of rounding the corners of the clipping rectangle. If you need a refresher on the basics of this function, you can check out the illustration below:

Illustration of how inset(d round r) works. Shows the clipping rectangle inside the element's border-box, its edges all a distance d away from the border limit. The corners of this clipping rectangle all have a rounding r along both axes.
How the inset() function works.

inset() cuts out everything outside a clipping rectangle defined by the distances to the edges of the element’s border-box, specified the same way we’d specify margin, border or padding (with one, two, three or four values) and the corner rounding for this rectangle, specified the same way we’d specify border-radius (any valid border-radius value is also valid here).

In our case, the distances to the edges of the border-box are all 0 (we don’t want to chop anything off any of the edges of the button), but we have a rounding that has to be at most at big as the border-width so that not having any inner border rounding makes sense.

$b: .25em;

button {
  /* same as before */
  border: solid $b transparent;
  clip-path: inset(0 round $b)
}

Note that the clip-path is also going to cut out any outer shadows we may add on the button element, whether they’re added via box-shadow or filter: drop-shadow().

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with small rounded corners, the rounding radius being the same size as the border-width. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The small corner rounding result using the border-image method (live demo).

While this technique cannot achieve the pill shape look, it does have the advantage of having great support nowadays and it may be all we need in certain situations.

The three solutions discussed so far can be seen compiled in the demo below, which also comes with a YouTube link where you can see me code each of them from scratch if you prefer to learn by watching things being built on video rather than reading about them.

All these methods create real transparency in the padding-box outside the text, so they work for any background we may have behind the button. However, we also have a couple of other methods that may be worth mentioning, even though they come with restrictions in this department.

The cover solution

Just like the border-image approach, this is a pretty limited tactic. It doesn’t work unless we have a solid or a fixed background behind the button.

It involves layering backgrounds with different background-clip values, just like the cover technique for gradient borders. The only difference is that here we add one more gradient layer on top of the one emulating the background behind our button element and we clip this top layer to text.

$c: #393939;

html { background: $c; } 

button {
  /* same as before */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid .25em transparent;
  border-radius: 9em;
  background: var(--grad) border-box, 
              linear-gradient($c 0 0) /* emulate bg behind button */, 
              var(--grad) border-box;
  -webkit-background-clip: text, padding-box, border-box;
  -webkit-text-fill-color: transparent;
}

Sadly, this approach fails in Firefox due to an old bug — just not applying any background-clip while also making the text transparent produces a pill-shaped button with no visible text.

Screenshot collage. Chrome (left) vs. Firefox (right). Chrome displays a pill-shaped pastel gradient ghost button. Firefox sadly only displays a pill-shaped button with no visible text.
The all background-clip cover solution (live demo).

We could still make it cross-browser by using the cover method for the gradient border on a ::before pseudo and background-clip: text on the actual button, which is basically just a more limited version of the second solution we discussed — we still need to use a pseudo, but, since we use a cover, not a mask, it only works if we have a solid or fixed background behind the button.

$b: .25em;
$c: #393939;

html { background: $c; } 

button {
  /* same base styles */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid $b transparent;
  background: var(--grad) border-box;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
  
  &::before {
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: linear-gradient($c 0 0) padding-box, 
                var(--grad) border-box;
    content: '';
  }
}

On the bright side, this more limited version should also work in pre-Chromium Edge.

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a solid background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The cover solution on a pseudo for a solid background behind the button (live demo).

Below, there’s also the fixed background version.

$f: url(balls.jpg) 50%/ cover fixed;

html { background: $f; } 

button {
  /* same as before */
  
  &::before {
    /* same as before */
    background: $f padding-box, 
                var(--grad) border-box
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a fixed image background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The cover solution on a pseudo for a fixed background behind the button (live demo).

Overall, I don’t think this is the best tactic unless we both fit into the background limitation and we need to reproduce the effect in browsers that don’t support masking, but support clipping the background to the text, such as pre-Chromium Edge.

The blending solution

This approach is another limited one as it won’t work unless, for each and every gradient pixel that’s visible, its channels have values that are either all bigger or all smaller than than the corresponding pixel of the background underneath the button. However, this is not the worst limitation to have as it should probably lead to our page having better contrast.

Here, we start by making the parts where we want to have the gradient (i.e. the text, icon and border) either white or black, depending on whether we have a dark theme with a light gradient or a light theme with a dark gradient, respectively. The rest of the button (the area around the text and icon, but inside the border) is the inverse of the previously chosen color (white if we set the color value to black and black otherwise).

In our case, we have a pretty light gradient button on a dark background, so we start with white for the text, icon and border, and black for the background. The hex channel values of our two gradient stops are ff (R), da (G), 5f (B) and f9 (R), 37 (G), 6b (B), so we’d be safe with any background pixels whose channel values are at most as big as min(ff, f9) = f9 for red, min(da, 37) = 37 for green and min(5f, 6b) = 5f for blue.

This means having a background-color behind our button with channel values that are smaller or equal to f9, 37 and 5f, either on its own as a solid background, or underneath a background-image layer we blend with using the multiply blend mode (which always produces a result that’s at least as dark as the darker of the two layers). We’re setting this background on a pseudo-element since blending with the actual body or the html doesn’t work in Chrome.

$b: .25em;

body::before {
  position: fixed;
  inset: 0;
  background: url(fog.jpg) 50%/ cover #f9375f;
  background-blend-mode: multiply;
  content: '';
}

button {
  /* same base styles */
  position: relative; /* so it shows on top of body::before */
  border: solid $b;
  background: #000;
  color: #fff;
  
  &::after {
    filter: brightness(0) invert(1);
    content: attr(data-ico);
  }
}

Note that making the icon fully white means making it first black with brightness(0) and then inverting this black with invert(1).

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped black and white (white border, white text, white emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The black and white button (live demo).

We then add a gradient ::before pseudo-element, just like we did for the first cross-browser method.

button {
  /* same styles as before */
  position: relative;
  
  &::before {
    position: absolute;
    z-index: 2;
    inset: -$b;
    border-radius: inherit;
    background: linear-gradient(to right bottom, var(--slist);
    pointer-events: none;
    content: '';
  }
}

The only difference is that here, instead of giving it a negative z-index, we give it a positive z-index. That way it’s not just over the actual button, but also over the ::after pseudo and we set pointer-events to none in order to allow the mouse to interact with the actual button content underneath.

Screenshot. Shows a pill-shaped gradient button with no visible text on top of a dark image background.
The result after adding a gradient pseudo on top of the black and white button (live demo).

Now the next step is to keep the black parts of our button, but replace the white parts (i.e., the text, icon and border) with the gradient. We can do this with a darken blend mode, where the two layers are the black and white button with the ::after icon and the gradient pseudo on top of it.

For each of the RGB channels, this blend mode takes the values of the two layers and uses the darker (smaller) one for the result. Since everything is darker than white, the resulting layer uses the gradient pixel values in that area. Since black is darker than everything, the resulting layer is black everywhere the button is black.

button {
  /* same styles as before */
  
  &::before {
    /* same styles as before */
    mix-blend-mode: darken;
  }
}
Screenshot collage.  Chrome (left) vs. Firefox (right). Both show a pill-shaped black and pastel gradient (pastel gradient border, text, emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The “almost there” result (live demo).

Alright, but we’d only be done at this point if the background behind the button was pure black. Otherwise, in the case of a background whose every pixel is darker than the corresponding pixel of the gradient on our button, we can apply a second blend mode, this time lighten on the actual button (previously, we had darken on the ::before pseudo).

For each of the RGB channels, this blend mode takes the values of the two layers and uses the lighter (bigger) one for the result. Since anything is lighter than black, the resulting layer uses the background behind the button everywhere the button is black. And since a requirement is that every gradient pixel of the button is lighter than the corresponding pixel of the background behind it, the resulting layer uses the gradient pixel values in that area.

button {
  /* same styles as before */
  mix-blend-mode: lighten;
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped pastel gradient ghost with a 'BOO!' text and a ghost emoji button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The light ghost button on top of a dark background (live demo).

For a dark gradient button on a light background, we need to switch up the blend modes. That is, use lighten on the ::before pseudo and darken on the button itself. And first of all, we need to ensure the background behind the button is light enough.

Let’s say our gradient is between #602749 and #b14623. The channel values of our gradient stops are 60 (R), 27 (G), 49 (B) and b1 (R), 46 (G), 23 (R), so the background behind the button needs to have channel values that are at least max(60, b1) = b1 for red, max(27, 46) = 46 for green and max(49, 23) = 49 for blue.

This means having a background-color on our button with channel values that are bigger or equal to b1, 46 and 49, either on its own as a solid background, or underneath a background-image layer, uses a screen blend mode (which always produces a result that’s at least as light as the lighter of the two layers).

We also need to make the button border, text and icon black, while setting its background to white:

$b: .25em;

section {
  background: url(fog.jpg) 50%/ cover #b14649;
  background-blend-mode: screen;
}

button {
  /* same as before */
  border: solid $b;
  background: #fff;
  color: #000;
  mix-blend-mode: darken;

  &::before {
    /* same as before */
    mix-blend-mode: lighten
  }
  
  &::after {
    filter: brightness(0);
    content: attr(data-ico);
  }
}

The icon in the ::after pseudo-element is made black by setting filter: brightness(0) on it.

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped dark gradient ghost with a 'BOO!' text and a ghost emoji button on top of a light image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The dark ghost button on top of a light background (live demo).

We also have the option of blending all the button layers as a part of its background, both for the light and dark theme, but, as mentioned before, Firefox just ignores any background-clip declaration where text is a part of a list of values and not the single value.

Well, that’s it! I hope you’re having (or had) a scary Halloween. Mine was definitely made horrific by all the bugs I got to discover… or rediscover, along with the reality that they haven’t been fixed in the meanwhile.


CSS-ing Candy Ghost Buttons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-ing-candy-ghost-buttons/feed/ 5 354804
How to Animate a SVG with border-image https://css-tricks.com/how-to-animate-a-svg-with-border-image/ https://css-tricks.com/how-to-animate-a-svg-with-border-image/#comments Thu, 03 Dec 2020 15:07:40 +0000 https://css-tricks.com/?p=326397 Let’s take a look at how to combine the border-image property in CSS with animated SVGs that move around a border. In the process, we’ll cover how to hand-craft resizable, nine-slice animated SVGs that you can use not only re-create …


How to Animate a SVG with border-image originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s take a look at how to combine the border-image property in CSS with animated SVGs that move around a border. In the process, we’ll cover how to hand-craft resizable, nine-slice animated SVGs that you can use not only re-create the effect, but to make it your own.

Here’s what we’re making:

Animated gif of red skulls moving around a list of high scores in a retro arcade font that go from first place to tenth place.
Spooky skulls? Retro arcade? What’s not to like?!

This is actually part of The Skull, a capture-the-flag riddle I’m working on that’s designed to explore the internals of Arduino and its microcontroller. I searched how to animate a border like this, but couldn’t find any useful examples. Most of the stuff I found was about marching ants, but unfortunately, the stroke-dasharray trick doesn’t work with skulls, let alone more complex shapes.

So, in the spirit of learning and sharing, I’m blogging about it here with you!

Should we use background or border-image?

At first, I didn’t even know border-image was a thing. I tried using a ::before pseudo-element in my first attempt, and animated its background-position property. That got me this far:

As you can see, it worked, but completing the border would require at least eight different elements (or pseudo-elements). Not ideal to clutter the HTML like that.

I posted a question on the Israeli CSS developers Facebook group, and everyone pointed me at the border-image property. It does exactly what it says on the tin: use an image (or CSS gradient) for an element’s border.

To work with border-image, you have to provide it an image which is used in a nine-slice way (think of a tic-tac-toe board over the image). Each of those nine regions represents a different part of the border: the top, right, left, and bottom, each of the four corners, and then the middle (which is ignored).

For instance, if we just wanted static skulls, we could take advantage of SVG patterns to repeat the skull nine times. First, we define an 24×24 pattern using the skull’s path, and then use this pattern as the fill for a 72×72 rect:

<svg version="1.1" height="72" width="72" xmlns="http://www.w3.org/2000/svg">
 <defs>
  <pattern id="skull-fill" width="24" height="24" 
patternUnits="userSpaceOnUse">
    <path d="..." fill="red"/>
  </pattern>
 </defs>
 <rect fill="url(#skull-fill)" width="72" height="72" />
</svg>

Next, we define a border and set the border-image on the target element:

.skulls {
  border: 24px solid transparent;
  border-image: url("https://skullctf.com/images/skull-9.svg") 24 round;
}

And we get a border made out of skulls:

Adding SVG animations

Now we can animate those skulls! It works, uh, for the most part.

The idea is to create a different animation for each region in the border image. For instance, in the top-left corner, we have one skull going from the right-to-left, while a second skull goes from top- to-bottom at the same time.

We’ll animate the transform property for the movement. We’ll also take advantage of SVG’s <use> to avoid repeating the lengthy <path> definition for each skull:

<svg version="1.1" height="96" width="96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 <style>
  @keyframes left {to {transform: translate(-32px, 0)}}
  @keyframes down {to {transform: translate(0, 32px)}}
 </style>
 <defs>
  <path id="skull" d="..." fill="red"/>
 </defs>

 <!-- Top-left corner: one skull goes left, another goes down -->
 <use href="#skull" x="0" y="0"  style="animation: down .4s infinite linear"/>
 <use href="#skull" x="32" y="0" style="animation: left .4s infinite linear"/>
</svg>

The SVG animation syntax might look familiar there, because rather than some SVG-specific syntax, like SMIL, it’s just using CSS animations. Cool, right?

This is what we get:

And if we add a grid, we can see how this animation also covers some of the top and left edges as well:

It starts looking more impressive after we add the remaining three edges, thus completely covering all the eight regions of the border image:

<svg version="1.1" height="96" width="96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 <style>
  @keyframes left {to {transform: translate(-32px, 0)}}
  @keyframes down {to {transform: translate(0, 32px)}}
  @keyframes right {to {transform: translate(32px, 0)}}
  @keyframes up {to {transform: translate(0, -32px)}}
 </style>
 <defs>
  <path id="skull" d="..." fill="red"/>
 </defs>

 <!-- Top-left corner: one skull goes left, another goes down -->
 <use href="#skull" x="0" y="0"  style="animation: down .4s infinite linear"/>
 <use href="#skull" x="32" y="0" style="animation: left .4s infinite linear"/>

 <!-- Top-right corner: one skull goes up, another goes left -->
 <use href="#skull" x="64" y="0" style="animation: left .4s infinite linear"/>
 <use href="#skull" x="64" y="32" style="animation: up .4s infinite linear"/>

 <!-- Bottom-left corner: one skull goes down, another goes right -->
 <use href="#skull" x="0" y="32" style="animation: down .4s infinite linear"/>
 <use href="#skull" x="0" y="64" style="animation: right .4s infinite linear"/>

 <!-- Bottom-right corner: one skull goes right, another goes up -->
 <use href="#skull" x="32" y="64" style="animation: right .4s infinite linear"/>
 <use href="#skull" x="64" y="64" style="animation: up .4s infinite linear"/>
</svg>

And this gives us a complete circuit:

Dancing skulls, ready to go into your border!

Putting everything together, we use the animated SVG we’ve just created as the border-image, and get the desired result:

I could play with this all day…

Once I got this to work, I started tinkering with the animation properties. This is one of the advantages of using SVGs instead of GIFs: changing the nature of the animation is as easy as changing one CSS property in the SVG source file, and you get to see the result instantly, not to mention the smaller file sizes (especially if you are dealing with gradients), full color support and crisp scaling.

For starters, I tried to see what it would like like if I changed the animation timing function to ease:

We can also make the skulls fade between red and green:

We can even make the skulls change orientation as they go around the high score table:

Head to the JavaScript tab where you can tinker with the SVG source and try it for yourself.

The giant ? in the room (cough, Firefox)

I was very happy when I first got this to work. However, there are some caveats that you should be aware of. First and foremost, for some reason, Firefox doesn’t render the animation at the edges of the border, only at the corners:

Funny enough, if I changed the SVG to a GIF with the same animation, it worked perfectly fine. But then the edges stop animating on Chrome! 🤦‍♂️

In any case, it seems to be a browser bug, because if we change the border-image-repeat property to stretch , Firefox does animate the edges, but the result is a bit quirky (though it can probably fit the theme of the page):

Changing the border-image-repeat value to space also seems to work, but only if the width of the element is not a whole multiple of the skull size, which means we get some gaps in the animation.

I also spotted a few visual issues in cases when the container size is not a multiple of the patch size (32px in this case), such as tiny black lines on the skulls. I suspect this has to do with some floating point rounding issue. It also tends to break when zooming in.


Not perfect, but definitely done! If you want to see the final version in action, you are invited to check out The Skull’s High Scores page. Hopefully, it’ll have some of your names on it very soon!


How to Animate a SVG with border-image originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-to-animate-a-svg-with-border-image/feed/ 11 326397
Stacked “Borders” https://css-tricks.com/stacked-borders/ https://css-tricks.com/stacked-borders/#comments Tue, 19 Mar 2019 14:42:40 +0000 http://css-tricks.com/?p=284681 A little while back, I was in the process of adding focus styles to An Event Apart’s web site. Part of that was applying different focus effects in different areas of the design, like white rings in the header and …


Stacked “Borders” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
A little while back, I was in the process of adding focus styles to An Event Apart’s web site. Part of that was applying different focus effects in different areas of the design, like white rings in the header and footer and orange rings in the main text. But in one place, I wanted rings that were more obvious—something like stacking two borders on top of each other, in order to create unusual shapes that would catch the eye.

A row of four images, the second of which includes a dashed red border.

I toyed with the idea of nesting elements with borders and some negative margins to pull one border on top of another, or nesting a border inside an outline and then using negative margins to keep from throwing off the layout. But none of that felt satisfying.

It turns out there are a number of tricks to create the effect of stacking one border atop another by combining a border with some other CSS effects, or even without actually requiring the use of any borders at all. Let’s explore, shall we?

Outline and box-shadow

If the thing to be multi-bordered is a rectangle—you know, like pretty much all block elements—then mixing an outline and a spread-out hard box shadow may be just the thing.

Let’s start with the box shadow. You’re probably used to box shadows like this:

.drop-me {
  background: #AEA;
  box-shadow: 10px 12px 0.5rem rgba(0,0,0,0.5);
}
A turquoise box containing the words div text and a heavy box shadow.

That gets you a blurred shadow below and to the right of the element. Drop shadows, so last millennium! But there’s room, and support, for a fourth length value in box-shadow that defines a spread distance. This increases the size of the shadow’s shape in all directions by the given length, and then it’s blurred. Assuming there’s a blur, that is.

So if we give a box shadow no offset, no blur, and a bit of spread, it will draw itself all around the element, looking like a solid border without actually being a border.

.boxborder-me {
  box-shadow: 0 0 0 5px firebrick;
}
A red box containing a thick red border.

This box-shadow “border” is being drawn just outside the outer border edge of the element. That’s the same place outlines get drawn around block boxes, so all we have to do now is draw an outline over the shadow. Something like this:

.boxborder-me {
  box-shadow: 0 0 0 5px firebrick;
  outline: dashed 5px darkturquoise;
}
A box containing a dashed red border with a turquoise background filling the dash gaps.

Bingo. A multicolor “border” that, in this case, doesn’t even throw off layout size, because shadows and outlines are drawn after element size is computed. The outline, which sits on top, can use pretty much any outline style, which is the same as the list of border styles. Thus, dotted and double outlines are possibilities. (So are all the other styles, but they don’t have any transparent parts, so the solid shadow could only be seen through translucent colors.)

If you want a three-tone effect in the border, multiple box shadows can be created using a comma-separated list, and then an outline put over top that. For example:

.boxborder-me {
  box-shadow: 0 0 0 1px darkturquoise,
              0 0 0 3px firebrick,
              0 0 0 5px orange,
              0 0 0 6px darkturquoise;
  outline: dashed 6px darkturquoise;
}
A box containing a dashed border where the dashes are doubled with red and gold and a turquoise background filling in the dash gaps.

Taking it back to simpler effects, combining a dashed outline over a spread box shadow with a solid border of the same color as the box shadow creates yet another effect:

.boxborder-me {
  box-shadow: 0 0 0 5px firebrick;
  outline: dashed 5px darkturquoise;
  border: solid 5px darkturquoise;
}
A box with a dashed red border and a turquoise background that not only fills the dash gaps, but overflows the red border toward the inside edge of the box.

The extra bonus here is that even though a box shadow is being used, it doesn’t fill in the element’s background, so you can see the backdrop through it. This is how box shadows always behave: they are only drawn outside the outer border edge. The “rest of the shadow,” the part you may assume is always behind the element, doesn’t exist. It’s never drawn. So you get results like this:

A box with a turquoise border and heavy box shadow toward the bottom right edge that is set against a turquoise background.

This is the result of explicit language in the CSS Background and Borders Module, Level 3, section 7.1.1:

An outer box-shadow casts a shadow as if the border-box of the element were opaque. Assuming a spread distance of zero, its perimeter has the exact same size and shape as the border box. The shadow is drawn outside the border edge only: it is clipped inside the border-box of the element.

(Emphasis added.)

Border and box-shadow

Speaking of borders, maybe there’s a way to combine borders and box shadows. After all, box shadows can be more than just drop shadows. They can also be inset. So what if we turned the previous shadow inward, and dropped a border over top of it?

.boxborder-me {
  box-shadow: 0 0 0 5px firebrick inset;
  border: dashed 5px darkturquoise;
}
A box with a dashed turquoise border and a solid red border that lies on the inside of the dashed border.

That’s… not what we were after. But this is how inset shadows work: they are drawn inside the outer padding edge (also known as the inner border edge), and clipped beyond that:

An inner box-shadow casts a shadow as if everything outside the padding edge were opaque. Assuming a spread distance of zero, its perimeter has the exact same size and shape as the padding box. The shadow is drawn inside the padding edge only: it is clipped outside the padding box of the element.

(Ibid; emphasis added.)

So we can’t stack a border on top of an inset box-shadow. Maybe we could stack a border on top of something else…?

Border and multiple backgrounds

Inset shadows may be restricted to the outer padding edge, but backgrounds are not. An element’s background will, by default, fill the area out to the outer border edge. Fill an element background with solid color, give it a thick dashed border, and you’ll see the background color between the visible pieces of the border.

So what if we stack some backgrounds on top of each other, and thus draw the solid color we want behind the border? Here’s step one:

.multibg-me {
  border: 5px dashed firebrick;
  background:
    linear-gradient(to right, darkturquoise, 5px, transparent 5px);
  background-origin: border-box;
}
A box with a dashed red border and a turquoise background filling in the dash gaps along the left edge of the box.

We can see, there on the left side, the blue background visible through the transparent parts of the dashed red border. Add three more like that, one for each edge of the element box, and:

.multibg-me {
  border: 5px dashed firebrick;
  background:
    linear-gradient(to top, darkturquoise, 5px, transparent 5px),
    linear-gradient(to right, darkturquoise, 5px, transparent 5px),
    linear-gradient(to bottom, darkturquoise, 5px, transparent 5px),
    linear-gradient(to left, darkturquoise, 5px, transparent 5px);
  background-origin: border-box;
}
A box with a dashed red border and a turquoise background that fills in the dash gaps.

In each case, the background gradient runs for five pixels as a solid dark turquoise background, and then has a color stop which transitions instantly to transparent. This lets the “backdrop” show through the element while still giving us a “stacked border.”

One major advantage here is that we aren’t limited to solid linear gradients—we can use any gradient of any complexity, just to spice things up a bit. Take this example, where the dashed border has been made mostly transparent so we can see the four different gradients in their entirety:

.multibg-me {
  border: 15px dashed rgba(128,0,0,0.1);
  background:
    linear-gradient(to top,    darkturquoise, red 15px, transparent 15px),
    linear-gradient(to right,  darkturquoise, red 15px, transparent 15px),
    linear-gradient(to bottom, darkturquoise, red 15px, transparent 15px),
    linear-gradient(to left,   darkturquoise, red 15px, transparent 15px);
  background-origin: border-box;
}
A diagram showing the same box with a dashed red border and turquoise background, but with transparency to show how the stacked borders overlap.

If you look at the corners, you’ll see that the background gradients are rectangular, and overlap each other. They don’t meet up neatly, the way border corners do. This can be a problem if your border has transparent parts in the corners, as would be the case with border-style: double.
Also, if you just want a solid color behind the border, this is a fairly clumsy way to stitch together that effect. Surely there must be a better approach?

Border and background clipping

Yes, there is! It involves changing the clipping boxes for two different layers of the element’s background. The first thing that might spring to mind is something like this:

.multibg-me {
  border: 5px dashed firebrick;
  background: #EEE, darkturquoise;
  background-clip: padding-box, border-box;
}

But that does not work, because CSS requires that only the last (and thus lowest) background be set to a <color> value. Any other background layer must be an image.

So we replace that very-light-gray background color with a gradient from that color to that color: this works because gradients are images. In other words:

.multibg-me {
  border: 5px dashed firebrickred;
  background: linear-gradient(to top, #EEE, #EEE), darkturquoise;
  background-clip: padding-box, border-box;
}
A box with a red dashed border, a turquoise background to fill in the dash gaps, and a light gray background set inside the box.

The light gray “gradient” fills the entire background area, but is clipped to the padding box using background-clip. The dark turquoise fills the entire area and is clipped to the border box, as backgrounds always have been by default. We can alter the gradient colors and direction to anything we like, creating an actual visible gradient or shifting it to all-white or whatever other linear effect we would like.

The downside here is that there’s no way to make that padding-area background transparent such that the element’s backdrop can be seen through the element. If the linear gradient is made transparent, then the whole element background will be filled with dark turquoise. Or, more precisely, we’ll be able to see the dark turquoise that was always there.

In a lot of cases, it won’t matter that the element background isn‘t see-through, but it’s still a frustrating limitation. Isn’t there any way to get the effect of stacked borders without wacky hacks and lost capabilities?

Border images

In fact, what if we could take an image of the stacked border we want to see in the world, slice it up, and use that as the border? Like, say, this image becomes this border?

An image of two stacked boxes, the top a square box with a red dashed border and a turquoise and gold striped background filling in the dash gaps. The bottom box is a demonstration using the top box as a border image for the bottom box.

Here’s the code to do exactly that:

.borderimage-me {
  border: solid 5px;
  border-image: url(triple-stack-border.gif) 15 / 15px round;
}

First, we set a solid border with some width. We could also set a color for fallback purposes, but it’s not really necessary. Then we point to an image URL, define the slice inset(s) at 15 and width of the border to be 15px, and finally the repeat pattern of round.

There are more options for border images, which are a little too complex to get into here, but the upshot is that you can take an image, define nine slices of it using offset values, and have those images used to synthesize a complete border around an image. That’s done by defining offsets from the edges of the image itself, which in this case is 15. Since the image is a GIF and thus pixel-based, the offsets are in pixels, so the “slice lines” are set 15 pixels inward from the edges of the image. (In the case of an SVG, the offsets are measured in terms of the SVG’s coordinate system.) It looks like this:

A diagram outlining how the border image is sliced and positioned along the box's edges, corner's and offsets.

Each slice is assigned to the corner or side of the element box that corresponds to itself; i.e., the bottom right corner slice is placed in the bottom right corner of the element, the top (center) slice is used along the top edge of the element, and so on.

If one of the edge slices is smaller than the edge of the element is long—which almost always happens, and is certainly true here—then the slice is repeated in one of a number of ways. I chose round, which fills in as many repeats as it can and then scales them all up just enough to fill out the edge. So with a 70-pixel-long slice, if the edge is 1,337 pixels long, there will be 19 repetitions of the slice, each of which is scaled to be 70.3 pixels wide. Or, more likely, the browser generates a single image containing 19 repetitions that’s 1,330 pixels wide, and then stretches that image the extra 7 pixels.

You might think the drawback here is browser support, but that turns out not to be the case.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
565011129.1

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
1151151159.3

Just watch out for the few bugs (really, implementation limits) that linger around a couple of implementations, and you’ll be fine.

Conclusion

While it might be a rare circumstance where you want to combine multiple “border” effects, or stack them atop each other, it’s good to know that CSS provides a number of ways to get the job done, and that most of them are already widely supported. And who knows? Maybe one day there will be a simple way to achieve these kinds of effects through a single property, instead of by mixing several together. Until then, happy border stacking!


Stacked “Borders” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/stacked-borders/feed/ 4 284681
Gradient Borders in CSS https://css-tricks.com/gradient-borders-in-css/ https://css-tricks.com/gradient-borders-in-css/#comments Fri, 28 Dec 2018 15:15:50 +0000 http://css-tricks.com/?p=280191 Let’s say you need a gradient border around an element. My mind goes like this:

  • There is no simple obvious CSS API for this.
  • I’ll just make a wrapper element with a linear-gradient background, then an inner element will block


Gradient Borders in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s say you need a gradient border around an element. My mind goes like this:

  • There is no simple obvious CSS API for this.
  • I’ll just make a wrapper element with a linear-gradient background, then an inner element will block out most of that background, except a thin line of padding around it.

Perhaps like this:

If you hate the idea of a wrapping element, you could use a pseudo-element, as long as a negative z-index value is OK (it wouldn’t be if there was much nesting going on with parent elements with their own backgrounds).

Here’s a Stephen Shaw example of that, tackling border-radius in the process:

You could even place individual sides as skinny pseudo-element rectangles if you didn’t need all four sides.

But don’t totally forget about border-image, perhaps the most obtuse CSS property of all time. You can use it to get gradient borders even on individual sides:

Using both border-image and border-image-slice is probably the easiest possible syntax for a gradient border, it’s just incompatible with border-radius, unfortunately.

DigitialOcean documents the same approach in another tutorial.


Gradient Borders in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/gradient-borders-in-css/feed/ 18 280191
Slicing SVG 9 Ways https://css-tricks.com/slicing-svg-9-ways/ Tue, 06 Sep 2016 14:00:49 +0000 http://css-tricks.com/?p=245253 Paul Lewis animated a 3D Card Flip. Straightforward enough there, but this needed a shadow element as well:

The shadow may need to move around or fade, so we need a solution that we can animate with transforms & opacity,


Slicing SVG 9 Ways originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Paul Lewis animated a 3D Card Flip. Straightforward enough there, but this needed a shadow element as well:

The shadow may need to move around or fade, so we need a solution that we can animate with transforms & opacity, since they can be handed off to the GPU. It will need to be separate to the element it’s shadowing, since we will likely want to fade and move it without affecting the other element.

He tried box-shadow and filter: blur(), but both aren’t performant enough.

He tried an SVG element with a blur filter, but SVG alone can’t do 9-slice scaling (necessary to keep the corners from scaling awkwardly).

The winner was using an SVG image but applying it to an element via border-image, which essentially 9-slice scaling.

To Shared LinkPermalink on CSS-Tricks


Slicing SVG 9 Ways originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
245253
Understanding border-image https://css-tricks.com/understanding-border-image/ https://css-tricks.com/understanding-border-image/#comments Wed, 21 Jul 2010 13:40:53 +0000 http://css-tricks.com/?p=6883 The new CSS3 property border-image is a little tricky, but it can allow you to create flexible boxes with custom borders (or drop shadows, if that’s your thing) with a single div and a single image. In this article, I …


Understanding border-image originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The new CSS3 property border-image is a little tricky, but it can allow you to create flexible boxes with custom borders (or drop shadows, if that’s your thing) with a single div and a single image. In this article, I explain how the border-image shorthand property works in today’s browsers.

The basic idea

The border-image shorthand property has 3 parts:

border-image: url(border-image.png) 25% repeat;

Essentially, these allow you to specify:

  1. An image to use as the border
  2. Where to slice that image, dividing the image into 9 sections
  3. How the browser should apply those sections to the edges of your element

The pertinent details

Let’s look at each part of the process in a little more detail. The first part is easy, and is familiar from the background-image property. For demonstration purposes I’ll use this image, which is 100px x 100px:

a border image

Slicing your image

The second part can have from one to four values, much like the border-width property, and they are specified in the same order: top, right, bottom, left. You can use percentages or pixels. Strangely, the percentages require the “%”, while pixels should be listed without the “px”:

border-image: url(my-image.gif) 25% 30% 10% 20% repeat;
border-image: url(my-image.gif) 25 30 10 20 repeat;

In this case, since my image is 100px x 100px, the two rules above are equivalent – they slice the image in the same places. I’ve added some dimensions on my image to demonstrate:

Repeat, Round, Stretch

border-image will always place the corner sections of your image into the corresponding corners of your element box, but the third part of the shorthand rule tells the browser how to treat the middle sections of your image – the ones that will go along the edges of your element.

Repeat (repeat, or tile, the image) and stretch (stretch, or scale, the image) are pretty self-explanatory.

Round means tile the image but only so that a whole number of tiles fit, and otherwise scale the image. Right now, Safari and Chrome interpret round as repeat. There can be up to two values: one for the top and bottom edges of the element, and one for the left and right.

Here’s an example with the top/bottom value set to repeat, and the left/right value set to stretch:

#example-one {
  border-width: 25px 30px 10px 20px;
  border-image: url("border-image.png") 25 30 10 20 repeat stretch;
}
Screenshot for Example One

LIVE DEMO, RSS READERS CLICK OVER TO SEE. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean eu arcu non dui consequat vestibulum non vitae eros. Donec imperdiet lorem at mi rhoncus lacinia. Phasellus porttitor ligula eu justo condimentum eget placerat arcu pharetra. Proin fringilla vulputate eros in accumsan. Sed mi nibh, pulvinar eu sollicitudin ut, feugiat non ipsum. In ornare, quam sit amet tempor suscipit, erat odio suscipit nisi, eu gravida nisl orci ut arcu. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Border-width

border-image won’t do anything if you don’t specify a width for your border. For browsers that understand border-image, your image slices will be scaled to the specified width. If you use the border shorthand property, it provides a nice fallback for browsers that don’t:

#example-two {
  border: 50px double orange;
  border-image: url("border-image.png") 25 30 10 20 repeat;
}
Screenshot of Example Two

Or you can specify each width individually (in this example I’ve specified widths such that the image slices aren’t scaled at all):

#example-three {
  border-color: orange;
  border-style: double;
  border-width: 25px 30px 10px 20px;
  border-image: url("border-image.png") 25 30 10 20 repeat;
}
Screenshot of Example Three

Using a plain border at the same widths as your border-image won’t always be ideal, however, so you may want to use conditional stylesheets to give IE some different border styles altogether.

Browser quirks

Predictably, IE doesn’t understand anything of border-image. Browsers that do support border-image only support the shorthand property, not all the individual properties that are described in the spec. Some potentially useful properties aren’t supported at all, especially border-image-outset, which would solve this problem.

Also, the default behavior is supposed to be to discard the center section of the image, and use the ‘fill’ keyword on the border-image-slice property to preserve it:

The fill keyword, if present, causes the middle part of the border-image to be preserved. (By default it is discarded, i.e., treated as empty.) (Read the spec)

But the current browser behavior is to preserve the middle, and there is no way to turn it off. Thus, if you don’t want your element’s content area to have a background, the center section of your image must be empty. However, you can use this filling behavior to your advantage, to create a box with a fancy border and background, with only one image.

Interactive demo

I built a border-image demo page to help me get my head around this complicated new set of CSS3 properties. You can pick an image, specify repeat, round, or stretch, and adjust the border-width and slicing. Enjoy!

Examples in the wild

  • Chris Spooner uses it to nice effect to give his images a custom border.
  • On the travel blog Go Pink Boots (Update: now offline) I built recently, I used it to add a torn-paper background to images.
  • I use it on my site The Web Designers’ Review of Books (Update: now offline) to highlight blockquotes.

If you have other examples on live sites, I’d love to see them. Leave a link in the comments!


Understanding border-image originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/understanding-border-image/feed/ 32 6883