csswg-drafts
csswg-drafts copied to clipboard
[css-images] @image rule for manipulating images
There have been a lot of issues over the years about manipulating an existing image in some way before it's used by CSS. Some examples from a quick search:
- https://github.com/w3c/csswg-drafts/issues/4706
- https://github.com/w3c/csswg-drafts/issues/3183
- https://github.com/w3c/csswg-drafts/issues/4996
- https://github.com/w3c/csswg-drafts/issues/2364
There are also a bunch of features we defined and never got implementor interest, such as filter() or background-image-transform.
With my author hat on, I've also stumbled on use cases where I wanted to transform images, even in simple ways such as being able to override an image's intrinsic dimensions while setting background-size to a different size. Or just essentially setting object-fit: cover on a background-image so I could use background-size: <percentage>{2} without distortion.
What if we could address all of these in one fell swoop by creating a new at-rule to define images that CSS can then access through the image() function?
Something like:
@image --foo {
src: url("foo.png");
aspect-ratio: 1 / 1;
width: 100vw;
object-fit: cover;
filter: blur(10px);
opacity: .5;
transform: rotate(5deg);
}
Which would then be used like:
background: image(--foo);
border-image: image(--foo); /* etc */
Since any <image> is allowed in src, this can be used to create variants of the same image:
@image --foo-larger {
src: image(--foo);
scale: 2;
}
The descriptors I envision being allowed in @image are:
src: <image>: Mandatory, sets the image we are manipulating. Can be any CSS<image>including gradients etc.width,height,inline-size,block-sizeto override intrinsic dimensions. Percentages resolve relative to original intrinsic size OR viewport (not sure which one makes more sense; at least the latter is always available)aspect-ratioto override intrinsic aspect ratiomarginto add (transparent) spacing around imageobject-fitopacityfiltertransformand friends (translate,scale,rotate)clip-pathmask
The src descriptor could also support setting the source to a different image depending on resolution, as a nicer to read alternative of image-set().
Instead of:
background-image: image-set( "foo.png" 1x,
"foo-2x.png" 2x,
"foo-print.png" 600dpi );
it would be:
@image --foo {
src: url("foo.png") 1x
url("foo-2x.png") 2x,
url("foo-print.png") 600dpi;
}
In fact, it would be nice if one could specify different descriptors depending on resolution, so that people could do things like:
@image --foo {
src: url("foo.png") 1x;
}
@image --foo {
src: url("foo.png") 2x;
filter: blur(10px);
}
In the future, we may even want to add descriptors providing a fallback, color space, or other metadata about images.
The advantages of this syntax I see are:
- It solves the problem using syntax that authors are already familiar with. Instead of needing to learn different ad hoc ways of transforming images that are specific to each use case, they only need to learn one
@imagerule and then they can even guess the descriptors they need as they are essentially a subset of existing CSS properties. - It's designed to be easy to extend in the future to solve more image manipulation use cases.
- While lengthier, it's nicer to read than functional syntax like
filter(). Compare:
background-image: filter(url("foo.png"), hue-rotate(135deg) opacity(.5));
with:
@image --foo {
src: url("foo.png");
filter: hue-rotate(135deg);
opacity: .5;
}
/* ... */
background-image: image(--foo);
The main downsides I see :
- With functional syntaxes like
filter()it's possible to usevar()references trivially, and interpolation works out of the box. It's unclear if it's possible to havevar()references resolve at the point of usage of the image, and interpolation may be trickier to define (but possible). - It has the same issues as
@propertywrt to global scope and shadow DOM (but that applies to most at-rules anyway and we likely need to fix it for all of them)
Big +1 here, I like this a lot. The fact that you can just use existing properties in easily-understood ways is a big plus here, both for authors and for us spec editors.
With functional syntaxes like filter() it's possible to use var() references trivially, and interpolation works out of the box. It's unclear if it's possible to have var() references resolve at the point of usage of the image, and interpolation may be trickier to define (but possible).
This is doable.
It has the same issues as @property wrt to global scope and shadow DOM (but that applies to most at-rules anyway and we likely need to fix it for all of them)
Already solved, yeah - this would define tree-scoped names.
Great stuff, but won't this increase the overall parse time of the styles. How is the performance metrics?
Re. different things depending on resolution, could you nest @media inside @image to do this? That would open all sorts of doors, without inventing another microsyntax.
Re. different things depending on resolution, could you nest @media inside @image to do this? That would open all sorts of doors, without inventing another microsyntax.
Agreed, this would be great. Do note that the syntax above is not a new microsyntax, it's taken directly from image-set(). It is therefore an old microsyntax 😁
Yeah, using the image-set syntax and semantics is much better than trying to reproduce it thru media queries; MQs fundamentally can't handle resolution negotiation.
would clip-path inside @image function as a spriting syntax?
would
clip-pathinside @image function as a spriting syntax?
I hadn't thought of that, but I don't see why not. Cool use case!
@booluw wrote:
Great stuff, but won't this increase the overall parse time of the styles. How is the performance metrics?
As I understand it, the parse time doesn't increase much. It's the generation of the image that slows down its display a bit. I'm not an implementer but I assume the effect to be comparable to applying the existing properties to the element the image is used on.
What I mean by that is that a filter: blur(10px); in an @image rule would have a similar effect on the performance as a filter: blur(10px); on an element.
The difference is that it's just the image that's affected and not a whole element. And the generated image can be cached and reused in different places.
Thinking a bit more about this, I think it makes sense to split the manipulations from the image itself. So we'd have an @image-manipulation rule and a corresponding manipulate-image() function we put the images through.
Taking @LeaVerou's example this would then look like:
@image-manipulation --foo {
aspect-ratio: 1 / 1;
width: 100vw;
object-fit: cover;
filter: blur(10px);
opacity: .5;
transform: rotate(5deg);
}
background: manipulate-image(url("foo.png"), --foo);
This has the advantage, that authors can easily apply the same manipulations to different images and that existing logic for loading/generating the images can be reused.
So instead of
@image --foo {
src: url("foo.png") 1x
url("foo-2x.png") 2x,
url("foo-print.png") 600dpi;
...
}
background-image: image(--foo);
for providing different image sources for the image manipulation you'd write
@image-manipulation --foo {
...
}
background-image: manipulate-image(
image-set( "foo.png" 1x,
"foo-2x.png" 2x,
"foo-print.png" 600dpi
),
--foo
);
Sebastian
I love it! Yes, reusable image manipulations are far better than my original proposal (assuming we bikeshed the names).
If border-radius/corner-shape would be supported folks could do something like these over sized background pills: 
@SebastianZ reuseable image manipulations would actually help in performance since the logic would be cached.
I have a faint memory from back in the day that border-image was originally being implemented of folks taking issue with the fact that a 3x3 grid of a single image was necessary for border-image-source and that they wished there were a way to do it with just one instance of that image. @image would finally solve this by leveraging background-repeat and making an element size 3x that of the image to generate the 3x3 grid from the single image.
ran across a situation on walmart.com where I think @image might help:

.category-link{
background-image: image(--foo);
background-repeat: no-repeat;
background-position: center center;
}
@image --foo {
width: 90%;
aspect-ratio: 1;
background-color: #e2edfb;
border-radius: 50%;
}
@jsnkuhn Note that the idea behind this feature is to allow manipulating existing images. Your example obviously is meant to create one, which I believe is out of scope of this proposal.
Having said that, you could still achieve it with what was proposed earlier. With the proposed @image-manipulation rule (and yes, @LeaVerou, we should find a better name for it) this could be done with
@image-manipulation --circle {
width: 190px;
aspect-ratio: 1;
clip-path: circle(closest-side);
}
.category-link {
background-image: manipulate-image(image(#e2edfb), --circle);
background-repeat: no-repeat;
background-position: center center;
}
And regarding border-radius and corner-shape, I agree that they could be part of this as well but their effect can also be achieved by using clip-path: circle() as shown above or clip-path: path() for other shapes than circles.
Sebastian
Btw. here are some name suggestions for rule and function name combinations:
@image-change/change-image()@image-edit/edit-image()@image-modification/modify-image()@image-alternation/alter-image()@image-adjust/adjust-image()@image-adaptation/adapt-image()@image-variation/vary-image()@image-mutation/mutate-image()@image-customization/customize-image()@image/image()(reusing the existingimage()function and introducing a new syntax to it may probably be a no-go, though)
Obviously some of them fit better than others, though I just wanted to get the ball rolling for that.
Sebastian
You could easily create new images by starting something like src: image(transparent) or src: linear-gradient(...)
@SebastianZ ~I think the @image-customization / customize-image() rule/function is better. Descriptive~
@image() / @image is better, please. And if a user would use a gradient instead, should be defined inside of @image() since an image is been 'created/manipulated' with that rule.
ran across a situation on walmart.com where I think
@imagemight help:
.category-link{ background-image: image(--foo); background-repeat: no-repeat; background-position: center center; } @image --foo { width: 90%; aspect-ratio: 1; background-color: #e2edfb; border-radius: 50%; }
Doesn't the width here causes a conflict with background-size property? Having two different widths might just leave room for buggy implementation.
Just a recap, as it seems, people have forgotten about or have different understandings of the proposals:
@LeaVerou's initial idea was to create an image and manipulate it with this at-rule by defining the image source via the src descriptor and applying manipulations to it via the other descriptors.
My idea was to just put the manipulation rules into the at-rule (without src descriptor), so you can reuse it for several images.
@LeaVerou wrote:
You could easily create new images by starting something like
src: image(transparent)orsrc: linear-gradient(...)
@booluw wrote:
@image() / @imageis better, please. And if a user would use a gradient instead, should be defined inside of@image()since an image is been 'created/manipulated' with that rule.
Did you think of the initial proposal or do you expect the src descriptor to be optional? If the latter, this would merge both proposals, i.e. allow to define a one-off manipulation when the src descriptor is provided or to reuse the manipulation by skipping the descriptor. Though that would also complicate the rule both for implementors and authors because it provides two different functionalities behaving different in different contexts.
Linear gradients (like any other images) are covered by both proposals.
In Lea's proposal:
@image --rainbow-circle {
src: linear-gradient(red, yellow, lime, blue, purple);
width: 400px;
aspect-ratio: 1;
clip-path: circle(400px);
}
.rainbow {
background-image: image(--rainbow-circle);
background-size: contain;
}
My proposal:
@image-manipulation --rainbow-circle {
width: 400px;
aspect-ratio: 1;
clip-path: circle(400px);
}
.rainbow {
background-image: manipulate-image(linear-gradient(red, yellow, lime, blue, purple), --rainbow-circle);
background-size: contain;
}
@booluw:
Doesn't the
widthhere causes a conflict withbackground-sizeproperty? Having two different widths might just leave room for buggy implementation.
@jsnkuhn expected the at-rule to work on the box model. Though the idea is to let it work on the image.
A width in that rule means to manipulate the image's intrinsic width. The same applies to height and aspect-ratio. So, if an image has a size of 1000px x 1000px and you apply width: 500px to it, it's intrinsic size is then 500px x 1000px. And that image can then be used with background-size. So it is like you initially provided an 500px x 1000px image.
(See the example above where the generated image has an intrinsic width of 400px but is then resized via background-size: contain based on the size of the element it is used in.)
And for clarity, providing a width or any other size-related descriptors shrink or stretch the image. If the image shall be cropped, then clip-path or object-fit should be used.
An issue that can arise is when images do not have an intrinsic size like gradients, colors or some forms of SVGs. For them it needs to be defined what happens when you provide percentages or other relative units like em.
Sebastian
Thanks @SebastianZ.
I think the sizes of those SVGs and gradients should be the values defined in the manipulation rule, since the image is being created. That is; the image has it own box model, before being added to that of the element it is being applied to. Which implies that all box-model properties also work on the image.
There's another interesting use-case, I've mentioned it to @LeaVerou at CSSDay, having the @image-manipulation add filters to the image and have the browser cache that, so that animating it later, using transform for instance, will not cause browsers to melt down.
We had these exp. at Wix, where when applying both at the same time caused havoc, but when animating a canvas that already applied the filters, i.e. with effects already composited, the results were order(s) of magnitude better.
So, the idea is to somehow hint to the browser that this image will-not-change, something like:
@image-manipulation --recolor-effect {
filter: url(recolor-filter.svg#filter);
will-change: none;
}
.bg-parallax {
background-image: manipulate-image(image(url(bg.webp)));
}
And then animate .bg-parallax without extra damage.
apologies for the confusion I think that some of the example ideas I've posted might fall better under something like element().
So let me give this another try:

We have one background image that is then rotated in different ways to create slightly visually different backgrounds for the different links.
@image-manipulation rotate-x-y {
transform: rotate(180deg);
}
@image-manipulation rotate-x {
transform: rotateX(180deg);
}
@image-manipulation rotate-y {
transform: rotateY(180deg);
}
a {background-image: url(yellow.webp);}
a:nth-of-type(2) { background-image: url(yellow.webp), rotate-x-y; }
a:nth-of-type(3) { background-image: url(yellow.webp), rotate-x; }
a:nth-of-type(4) { background-image: url(yellow.webp), rotate-y; }
in this case having to repeat the background-image: url(yellow.webp), bit seems a bit clunky. Maybe there a way to do something more like manipulation-name taking a cue from the already existing animation-name property?
@booluw wrote:
I think the sizes of those SVGs and gradients should be the values defined in the
manipulation rule, since the image is being created.
You are right that a new image is created by manipulating the source image. And with width, height and aspect-ratio you can define the sizes of the manipulated image.
Though my question was how would a width: 80%; be interpreted when the source image doesn't have an intrinsic width? I guess, the answer in that case is that the width of the manipulated image is undefined as well.
That is; the image has it own box model, before being added to that of the element it is being applied to. Which implies that all box-model properties also work on the image.
I wouldn't say that the image has its own box model. There are properties like padding or box-sizing that don't apply to it.
@ydaniv wrote:
There's another interesting use-case, I've mentioned it to @LeaVerou at CSSDay, having the
@image-manipulationadd filters to the image and have the browser cache that, so that animating it later, usingtransformfor instance, will not cause browsers to melt down.
I expected the images generated by an @image-manipulation rule to always be cached because the manipulations are only applied once to an image. Or are there use cases, in which dynamically applying the rules is required?
@jsnkuhn wrote:
We have one background image that is then rotated in different ways to create slightly visually different backgrounds for the different links.
@image-manipulation rotate-x-y { transform: rotate(180deg); } @image-manipulation rotate-x { transform: rotateX(180deg); } @image-manipulation rotate-y { transform: rotateY(180deg); } a {background-image: url(yellow.webp);} a:nth-of-type(2) { background-image: url(yellow.webp), rotate-x-y; } a:nth-of-type(3) { background-image: url(yellow.webp), rotate-x; } a:nth-of-type(4) { background-image: url(yellow.webp), rotate-y; }in this case having to repeat the
background-image: url(yellow.webp),bit seems a bit clunky. Maybe there a way to do something more likemanipulation-nametaking a cue from the already existinganimation-nameproperty?
The proposed syntax would actually look like this:
a:nth-of-type(2) { background-image: manipulate-image(url(yellow.webp), rotate-x-y); }
a:nth-of-type(3) { background-image: manipulate-image(url(yellow.webp), rotate-x); }
a:nth-of-type(4) { background-image: manipulate-image(url(yellow.webp), rotate-y); }
I know, that makes it even longer to write.
Though manipulate-image() generates an <image> value. That value can be used in many different properties like border-image-source, mask-image, list-style-image, etc. And each one could have its own image manipulation applied. Therefore, a property like manipulation-name (or rather background-image-manipulation) that is bound to another property wouldn't make sense.
Though I assume the final name for the image manipulation function probably won't be that long.
Sebastian
@SebastianZ
I expected the images generated by an @image-manipulation rule to always be cached because the manipulations are only applied once to an image. Or are there use cases, in which dynamically applying the rules is required?
Other use cases are the old filter() and others mentioned at the top, which could potentially be animated.
So does this rule imply immutability of the result without the ability to animate its properties?
I guess further animations could be applied in properties outside of the manipulation effects, which is reasonable.
@SebastianZ
You are right that a new image is created by manipulating the source image. And with
width,heightandaspect-ratioyou can define the sizes of the manipulated image. Though my question was how would awidth: 80%;be interpreted when the source image doesn't have an intrinsic width? I guess, the answer in that case is that the width of the manipulated image is undefined as well.
I think a width: 80% should be interpreted in relation to the width of the elment the image is been applied to.
@ydaniv wrote:
I expected the images generated by an @image-manipulation rule to always be cached because the manipulations are only applied once to an image. Or are there use cases, in which dynamically applying the rules is required?
Other use cases are the old
filter()and others mentioned at the top, which could potentially be animated. So does this rule imply immutability of the result without the ability to animate its properties? I guess further animations could be applied in properties outside of the manipulation effects, which is reasonable.
I see. Regarding the filter() function, interpolation (and with that animation) is already defined in the Filter Effects specification.
Though I can now see the point for allowing to animate them. One way to achieve that with the proposed syntax would be:
@image-manipulation --rotate-image {
transform: rotate(359deg);
}
@keyframes --rotating-background {
from: { background-image: url("background.jpg"); }
to: { background-image: manipulate-image(url("background.jpg"), --rotate-image); }
}
.rotating-background {
animation: 35.9s --rotating-background infinite;
}
Alternatively, we might define the animation directly within the at-rule, i.e. allow the animation-* properties as descriptors for the at-rule.
Though as far as I know, the descriptors of at-rules aren't animatable so far. So that would be a novelty. @LeaVerou Feel free to correct me on this.
So an example for that could then look like this:
@keyframes --rotate {
from: { transform: rotate(0deg); }
to: { transform: rotate(359deg); }
}
@image-manipulation --rotate-image {
animation: 35.9s --rotate infinite;
}
.rotating-background {
background-image: manipulate-image(repeating-url("background.jpg"), --rotate-image);
}
@booluw wrote:
You are right that a new image is created by manipulating the source image. And with
width,heightandaspect-ratioyou can define the sizes of the manipulated image. Though my question was how would awidth: 80%;be interpreted when the source image doesn't have an intrinsic width? I guess, the answer in that case is that the width of the manipulated image is undefined as well.I think a
width: 80%should be interpreted in relation to the width of the elment the image is been applied to.
Making the manipulations dependent on the element they are used in, would cause significant performance issues as mentioned by @ydaniv. So if you animated the element's width, you'd have to run the manipulation rules for each frame of the animation and each element the rules are applied to.
Sebastian
Given the very positive feedback so far, let's add this to the agenda. Points to discuss:
- Allow image creation or just manipulation?
- Allow animations and transitions? If so, within or outside the at-rule?
- Performance concerns
- Names combination (see my previous comment, but bikeshedding for the names could also be done later)
- Which level of the spec.?
Sebastian
If this feature was about image creation, it should not be limited to a single layer, i.e. it would require nesting or additive sources.
@image --composite {
@layer --stage {
src: "backdrop.jpg";
}
@layer --scene {
src: "parallax.png";
}
@layer --actor {
src: "map.svg#sprite";
position: 200px 300px;
}
}
I believe this would be suggested by the generic name @image. Otherwise, a different and more specific name should be chosen, e.g. @image-effect.
This issue is about image manipulation, i.e. creating variations of existing images. This is what the very first sentence of it says.
I am not completely against allowing the at-rule to also create completely new ones, though I am strongly in favor of restricting it to manipulation and rely on the existing methods to provide the source images, namely url(), image-set(), image(), *-gradient(), etc.
This allows to provide context sensitive images. And it makes implementations easier as they just have to provide one way of image sources.
Sebastian
Ah, and the use case of compositing images is a different one which should be discussed separately.
Sebastian