Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-backgrounds] background-clip: border-area #9456

Open
LeaVerou opened this issue Oct 11, 2023 · 13 comments
Open

[css-backgrounds] background-clip: border-area #9456

LeaVerou opened this issue Oct 11, 2023 · 13 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Oct 11, 2023

Lots of use cases around specifying a continuous image across the border area, including gradient borders, patterned borders etc.

image image image image image image

From a quick search:

Many of these use cases cannot be done with the 9-slice scaling of border-image, and for others border-image introduces unnecessary restrictions and additional complexity, when essentially all that is needed is to be able to cut the padding-box part of the background out.
Today some of these can be achieved by specifying a border-box background and overlaying a padding-box background with the same color as the backdrop, but that doesn't work in every case. Authors use pseudo-elements to achieve some of these effects.

Why not a syntax to subtract from the area defined by background-clip?

Reading the description above, it's conceivable to ask: If what is needed is the ability to subtract padding-box from border-box, why not just add a syntax to subtract background areas? It could be background-clip syntax (e.g. background clip: border-box - padding-box) or a separate background-subtract: <box># property.

The reason is this introduces 4 * 3 = 12 combinations that need to be implemented and tested, only one of which is actually useful. 6 of which would never even produce an area (e.g. content-box - border-box) and 4 of which (text - *) would almost never produce an area, yet introduce unfathomable implementation complexities.

A single border-area addresses the vast majority of use cases, and keeps implementation complexity in check.

@Afif13
Copy link

Afif13 commented Oct 11, 2023

I will add one of my most active (and searched) Stack Overflow answer around this topic: https://stackoverflow.com/a/51496341/8620333

@Afif13
Copy link

Afif13 commented Oct 11, 2023

One use case would be the classic border-only pie chart where we can rely on conic-gradient() but we need to mask the inside part: https://codepen.io/t_afif/pen/XWaPXZO

Pie Chart CSS only

With the above, the code can be as simple as:

.pie {
  --p: 20; /* the progress*/
  --c: red;
  border: 10px solid #0000;
  border-radius: 50%;
  background: conic-gradient(var(--c) calc(var(--p)*1%),#0000 0) border-area
}

(for the rounded part, two extra radial-gradient will do the job and no need to clip them)

@nt1m
Copy link
Member

nt1m commented Oct 12, 2023

I don't think this is necessarily the ideal solution because once you clip the content inside the element is invisible, only the border remains. So you would need a second element for the actual content. I see this more as a border-image extension. I also wonder if mask-border covers this.

@thebabydino
Copy link

thebabydino commented Oct 12, 2023

One use case would be the classic border-only pie chart where we can rely on conic-gradient() but we need to mask the inside part: https://codepen.io/t_afif/pen/XWaPXZO

(for the rounded part, two extra radial-gradient will do the job and no need to clip them)

Love this use case! Now with animation of custom properties via @property finally coming to Firefox (soonish, I hope!), we could animate both the conic-gradient() and the second radial-gradient() by animating --p.


I don't think this is necessarily the ideal solution because once you clip the content inside the element is invisible, only the border remains. So you would need a second element for the actual content. I see this more as a border-image extension. I also wonder if mask-border covers this.

This property is background-clip, it doesn't clip the content, only the background. The content is unaffected. Are there use cases where extending border-image is more appropriate? Yes, but I think there are also use cases for a border-area value for background-clip.


The main advantage that I see over border-image is in the limitations of border-image. border-image doesn't allow layering multiple gradients with different background-size and/or background-position values in order to create border patterns and it doesn't work together with border-radius.


The first use case that came to my mind was that of gradient ghost buttons. I wrote an article detailing it a couple of years back https://css-tricks.com/css-ing-candy-ghost-buttons/

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 article dissects multiple methods of achieving this they are either more complicated than what this proposal would allow for or they're less than ideal due to using either non-standard, non-cross-browser methods or due to not achieving the exact result we're after. Or both.

This proposal would allow us to create a gradient sized relative to the border-box (background-origin: border-box), layer it twice and clip one layer to the border-area and one layer to text. Since it's the same gradient for both layers and the two areas we clip it to don't intersect, then the Firefox bug 1481498 can't visually break our result.

border: solid var(--b) transparent;
--g: linear-gradient(90deg, #ffda5f, #f9376b) border-box;
background: 
	var(--g) text, 
	var(--g) border-area;
color: transparent

The 1st method dissected in the article is layering and XORing three fully opaque mask layers. The first layer is restricted to the border-box, the second one to the padding-box and XOR-ing them basically gives us the border-area gradient. Then we XOR this intermediate result with the third fully opaque layer, which is restricted to text. And here is where we come across the first problem, text is a non-standard value for mask-clip and isn't supported cross-browser, so this solution only works in Chrome.

The 2nd method uses an extra pseudo with the first two mask layers from before to create the gradient border and also the same gradient clipped to text on the actual element. This works cross-browser, but has the disadvantage of using up an extra pseudo.

The 3rd method uses border-image. This is pretty simple, however, we cannot use this method to achieve the pill shape of the button as radial-gradient and border-image don't play nice together - play with checking/ unchecking the two properties, they both work separately, but not together. This is something else I'd like to see changing, but more on that a bit further down. We can get a tiny corner rounding --r via clip-path: inset(0 round var(--r)), but this corner rounding needs to be at most as big as the border-width, otherwise things will look weird.

The 4th method uses blending. This is very limited when it comes to the backgrounds we can use behind the ghost buttons. Even worse, we can only make it work in Firefox by using up an extra pseudo for it, as it fails there due to bug 1481498 (mentioned above).

So overall, this is one use case where having a border-area value for background-clip makes a lot of sense.


The second use case that comes to mind is "gradient" borders for images obtained out of the image source.

Let's say we have an image:

<img src='myImage.jpg'>

And let's say that we want a spaced out gradient border that goes with the image itself.

Images surrounded by a gap then by a gradient border whose colors are obtained from the image.

border: solid 1em transparent; /* create the border space */
padding: 1em; /* space between image and border */
background: filter(src(attr(src)), blur(50px)) 50%/ cover border-area

There's so much stuff that is poorly supported/ doesn't work in any browser in the code above, but oh, well...

We could of course do this with border-image too, but if we want rounded corners, we're back at the border-image doesn't work with border-radius problem.

And what we need to do nowadays to get such a result is a bit like scratching with the foot behind the ear https://codepen.io/thebabydino/pen/VwqEapG


A demo with pattern borders + rounded corners + (semi)transparent backgrounds examples (Lea might know those gradients, as they're adaptations of some of those from her 10+ year old CSS 3 Patterns gallery).

This is not achievable with border-image, which only accepts one image, cannot take the multiple gradient layers that were used to create these border patterns.

Screenshot


There are also gradient border situations where this wouldn't work however and that's dashed/ dotted gradient borders. There are various ways we could achieve them, but some are terribly hacky and awful (examples one, two, three), others are limited (using a repeating-conic-gradient in the mask to get a dashed gradient border for around a circular disc or round mask sizing to create the gradient dashes on a rectangular element with no rounded corners - examples one, two).

There's no good, consistent CSS way for getting dashed/ dotted gradient borders on elements that maybe have rounded corners without being circular.

Maybe some extension of border-image as suggested above would be better in such cases? One that can work together with border-radius and only paints the gradient in the dashes/ dots if a dashed/ dotted border was specified. If we want dashes/ dots, that makes it less likely we want the kind of complex patterns we cannot achieve with border-image for the reasons explained above.

@fvsch
Copy link

fvsch commented Oct 12, 2023

Love the idea. Definitely something that I’ve needed before.

I like the “quick win” aspect of this proposal, but worry that by using the background as a border it could create issues.

Possible accessibility concern: in a non-supporting UA, users may end up with text contrast issues when the background intended for the border is not clipped.

section {
  border: solid 10px transparent;
  color: black;
  background: linear-gradient(to bottom, black, gray);
  background-clip: border-area;
}

This can be avoided when authors make use of @supports:

section {
  border: solid 10px transparent;
  color: black;
}

@supports (background-clip: border-area) {
  section {
    background: linear-gradient(to bottom, black, gray);
    background-clip: border-area;
  }
}

But we can expect that many authors will not do that (usually because they were testing in a supporting browser, and didn’t realize this was a need). So a better failure mode would be great.

Another related concern is how High Contrast / Forced Colors modes would handle this. Maybe better if they force the background color or replace it with a backplate, and force the border color too.

@kizu
Copy link
Member

kizu commented Oct 12, 2023

@fvsch Yes, it has the same issue as background-clip: text, which has the very similar accessibility concerns mentioned on the MDN page: https://developer.mozilla.org/en-US/docs/Web/CSS/background-clip#accessibility_concerns (though with text it might be even more pronounced for cases where the background does not load at all, resulting in a transparent text).

I'm not sure if there is a good way to fix this outside of the @supports. Actually, I wish there was a way to require wrapping the property with @supports. Like, the feature won't work unless there is a @supports around it?

This way it would be possible to do bad by wrapping only the background-clip property with it, but I think it could be enough of a reminder for any developer that maybe they would need to think this through a bit more.

@LeaVerou
Copy link
Member Author

@nt1m As @thebabydino mentioned, this does not clip the element at all, it's a background property. No, border-clip does not cover it, because that clips the border, it doesn't provide a way to specify gradients and patterns that are only applied to the border. stroke covers a lot of it, but that its a much more complex feature that has so far seen little implementor interest.

@thebabydino Your post made me wonder if what we actually need is background-clip: border instead of border-area, i.e. something that clips to the actual border shape specified by border-style. This satisfies strictly more use cases, since you can always get the border-area behavior by specifying a solid border. Main downside is that it increases implementation complexity, though I’m not sure by how much (@nt1m @emilio ?)

@fvsch Your code suffers from this issue primarily because it's specifying background-clip separately. If it used the shorthand things just take care of themselves automatically, without @supports:

background: linear-gradient(to bottom, black, gray) border-area;

This reminded me, if we add this we should specify that if it's used in the shorthand as a single value it sets background-origin to border-box, since there's no corresponding background-origin value. This also increases the value-add for using the shorthand (since the vast majority of use cases require background-origin: border-box), making authors more likely to favor it over separate longhands.

@kizu It does not have the same issue as background-clip: text because it does not require making text transparent (only the border).


Also CCing my css-backgrounds-4 co-editors @fantasai @SebastianZ

@dharmi04
Copy link

If you have black theme then you can also use neon effects in background of image. It would look great!!

@seyedi
Copy link
Contributor

seyedi commented Nov 27, 2023

I created a video to show how much we need such a feature in CSS!

The best way to create animated gradient borders in CSS

@LeaVerou
Copy link
Member Author

Ok, given the support expressed in the reactions, and the pervasiveness of the use cases, I'm gonna Agenda+ this.

Issues to decide on:

  1. Do we want something like this?
  2. Should it be a keyword that clips to the border area, or to the border shape? There are use cases for both, and unlike what I originally thought, the use cases satisfied by the former are not a subset of the use cases satisfied by the latter (since we don't have multiple borders). The vast majority of use cases are satisfied by both as they just need a solid border that follows border radius, so if the latter involves signficantly higher eng effort, we should just go for the former (nothing precludes adding the latter later).

@SebastianZ
Copy link
Contributor

@LeaVerou Sorry for not answering earlier!

Allowing to clip the background image to the border area is one approach. Though personally, I find it a little hacky to reuse background images for that. I'd rather like to be able to turn off slicing to achieve this, so something like border-image-slice: none;.
We then still have to make borders respect border-radius and allow to define multiple border images, though that feels cleaner to me.

Also note #9183 as a tangential issue.

Sebastian

@fantasai
Copy link
Collaborator

@SebastianZ Aside from the name matching more closely, what would be the benefit of going that route? It seems like a lot of complication to add to border-image, is all of that really needed?

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-backgrounds] background-clip: border-area, and agreed to the following:

  • RESOLVED: add background-clip: border-area
The full IRC log of that discussion <emilio> lea: There's a bunch of examples in the thread
<emilio> ... happy to find more if the use cases are not obvious to everybody
<emilio> ... use case is image border but without the border-image scaling, they want a continuous border
<emilio> ... looks like a background image that is clipped only for the border-area
<emilio> ... even though it's not conceptually the best way to do it it seems straight-forward
<emilio> ... proposal is background-clip: border-box area
<emilio> ... which clips to border-box - padding-box
<emilio> ... a better decomposition might be allowing other substraction
<fantasai> +1 to adding border-area to background-clip, this seems straightforward and useful
<emilio> ... but since this is the only use case this seems simpler
<emilio> q+
<astearns> ack emilio
<iank_> q+
<dholbert> emilio (IRC): this is kind of a clip, but an outside clip with an inside clip... seems fine.
<dholbert> emilio (IRC): I'm curious if you've found a use-case with a semitransparent background
<dholbert> lea (IRC): I think it can be conceivable to have a semitransparent bg overlaid on another bg
<dholbert> lea (IRC): or even have it on the outside, and clip the bg to padding-box
<lea> q+ just remembered another design decision to make (clip to border-style or not?)
<dholbert> emilio (IRC): I'm looking at an example with oriol. There's a way to do this if you just draw a solid background on the same element
<lea> q+
<dholbert> emilio (IRC): seems fine to do this. thinking about use-cases for translucent elements.
<dholbert> emilio (IRC): Presumably this wouldn't interfere with border-image?
<dholbert> fantasai (IRC): it'd be underneath
<dholbert> emilio (IRC): seems fine I think
<astearns> ack iank_
<emilio> iank_: What happens with scrollable areas?
<emilio> fantasai: same that happens with backgrounds, it's just a background
<emilio> ... it's clipping out the entire padding area
<schenney> q+
<emilio> ... so you're only drawing the background inside the border area
<astearns> ack lea
<emilio> lea: one thing I'm not sure about is clipping border-box - padding-box
<emilio> ... not sure if we have use cases for other boxes
<emilio> ... someone pointed out that you can use a solid background over the padding box
<astearns> ack dbaron
<emilio> ... but that is not quite general because it might not be a solid color
<emilio> dbaron: I just want to clarify that this is clipping to border-area but it's not affected by border-style
<dbaron> dashed/dotted/double
<emilio> lea: yes, we might get use cases for that but this proposal covers most use cases
<astearns> ack schenney
<emilio> schenney: re. scrolling backgrounds, what about background-position: fixed?w
<emilio> s/w//
<emilio> lea: I think fixed is fixed to the viewport, local is what you are pointing out
<emilio> ... it'd move like it does now
<emilio> schenney: in the local area you don't want to clip necessarily to the padding box
<iank_> q+
<astearns> ack fantasai
<emilio> fantasai: you'd use a repeating pattern and you're painting that on the border box and subtracting the padding box
<astearns> ack iank_
<emilio> schenney: not sure what'd happen, and I know Chromium's background code very well
<emilio> iank_: one thing to consider is interaction with table parts
<emilio> ... what does border collapsing mean with this feature, columns
<emilio> fantasai: I think that falls down from the definitions we already have
<astearns> seems like we should have specified how borders work in all these cases
<emilio> ... there's a border-box we paint into and a padding-box we paint into
<emilio> ... might be that we don't paint anything if they are the same
<emilio> iank_: as long as the spec is clear
<emilio> fantasai: if it's unclear it's unclear about everything that already exists
<emilio> iank_: might be worth mentioning in an out about the expected interaction
<emilio> ... so that they explicitly think about this case
<emilio> florian: even better with test-cases
<emilio> iank_: sure, both!
<emilio> nicole: seems weird to think when a border starts with collapsing
<fantasai> If you paint `background: url(red-swatch) border-box, url(blue-swatch) padding-box`, the part that's red is the part we're proposing to clip to
<emilio> fantasai: I implemented something and got ripped out IIRC, I think right now there's no border
<fantasai> s/no border/no border area for internal table elements/
<emilio> astearns: so proposal is to add a border-area background-clip
<emilio> lea: this is nothing new, it uses existing concepts
<fantasai> s/table elements/table elements in collapsed borders mode/
<emilio> schenney: the result would be the result of background-clip: border-box - background-clip: padding-box right?
<emilio> ... that makes the impl very clear
<emilio> lea: there's an example in IRC from fantasai where that explains
<emilio> RESOLVED: add background-clip: border-area
<lea> 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Feb 2024 Agenda
Wednesday morning
Development

No branches or pull requests