w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 660 forks source link

[css-link-params] Let’s fix icons on the Web! Aka a way to pass currentColor and other context to linked SVGs #9872

Open LeaVerou opened 9 months ago

LeaVerou commented 9 months ago

Problem statement & Motivation

This is an area of the web platform where small improvements could save authors a lot of pain.

Right now, authors frequently want to reuse vector icons with different colors for different parts of their UI (often even changing the color with user interactions). The current ways to do that are:

  1. Inline SVG with <use> element, so that its color can change via currentColor.
  2. Inline SVG that simply embeds the whole icon
  3. Icon font. This allows changing the icon color via CSS, but has a ton of other drawbacks.

For example, look at the ways Bootstrap Icons and Font Awesome recommend for embedding. All this complication is done entirely to pass currentColor to the icon!!

While color is the primary use case, there are other context aspects that it would be useful to be able to pass to linked files, such as:

Prior art

There have been several SVG Params specs, but none moved forwards:

  1. Doug’s old SVG Parameters based on URL parameters and <param> elements in <object>
  2. Tab’s more recent SVG Params based on custom properties

These are strictly more powerful, and would solve a superset of use cases, but are also far more complex to implement.

Proposal

I want to propose something much simpler: A way to pass aspects of the page’s CSS context to an SVG linked via CSS (and hopefully later backport to HTML <img>). We could even start entirely with currentColor, since that’s by far the biggest author pain point.

We could extend src() with a second argument for passing parameters to the resource being embedded, possibly after a with keyword (inspired from JS Import assertions).

Then, we could either use a series of keywords (where color passes currentColor) or a context() function that takes a series of keywords.

content: src("icon.svg" with color);
content: src("icon.svg" with context(color, font, color-scheme)) 

The keywords themselves could be named after CSS properties (e.g. color, font) or values (e.g. currentColor).

content: src("icon.svg" with context(currentColor)) 

I’m proposing extending src() only since its parsing is simpler, but we may want to extend url() as well (when strings are used for the URL).

dbaron commented 9 months ago

Another piece of relevant prior art (though on the simpler side rather than the more complex side) is the context-fill and context-stroke keywords in SVG2, which are implemented in Gecko. (Also see Gecko bug 1058040 and Chromium bug 367737.)

tabatkins commented 9 months ago

There's no reason to use the with keyword; the url functions already have a syntax for additional parameters, and it can be just context(), like

content: src("icon.svg", context(currentcolor))`
/* or maybe */
content: src("icon.svg", with(currentcolor))`

I think this is a good idea, but it could benefit from hewing more closely to my Params proposal:

  1. You might want to set the currentcolor/etc to a specific value, not just what's defined on the element. So the function should allow an optional value after each keyword. So like src("icon", with(currentcolor red, font-size 20px)).
  2. There should be a way to trigger this via url as well, so you can use it in <img src="icon.svg">.

Maybe we can just merge this into the Params proposal, so you have a set of predefined non-dashed param names you can use, in addition to using dashed idents to set variables.

(Then we can also make the value after the dashed param name optional, defaulting to taking the value of the same-named variable on the element, for cases where that name matches up.)

LeaVerou commented 9 months ago

I think this is a good idea, but it could benefit from hewing more closely to my Params proposal:

  1. You might want to set the currentcolor/etc to a specific value, not just what's defined on the element. So the function should allow an optional value after each keyword. So like src("icon", with(currentcolor red, font-size 20px)). and Maybe we can just merge this into the Params proposal, so you have a set of predefined non-dashed param names you can use, in addition to using dashed idents to set variables.

The main motivation for this proposal was for it to be a quick win, something that can be implemented more easily than the full-blown params proposal. As long as tying it to the params proposal doesn't negate that, I think it’s a good idea to integrate them more closely to avoid the cognitive overhead on users of having to learn two distinct syntaxes for highly related things.

  1. There should be a way to trigger this via url as well, so you can use it in <img src="icon.svg">.

Agreed but this seems like a whole different feature (and far more complicated to define in a way that doesn't clash with existing params).

(Then we can also make the value after the dashed param name optional, defaulting to taking the value of the same-named variable on the element, for cases where that name matches up.)

I love this!

tabatkins commented 9 months ago

something that can be implemented more easily than the full-blown params proposal.

All that Params does is set the initial value of variables. It's the exact same feature as what you're proposing, just affecting a different set of things, so they should be equally easy or difficult.

Agreed but this seems like a whole different feature (and far more complicated to define in a way that doesn't clash with existing params).

Right, this is work Params has already done, so there's no need to do additional work. Just write <img "icon.svg#param(currentcolor, font-size, --shadow-color blue)"> to pass in the currentcolor and font-size from the element, and set the --shadow-color variable to blue.

SebastianZ commented 9 months ago
  1. Tab’s more recent SVG Params based on custom properties

Just to note, CSS Linked Parameters is already on the standards track.

I do like the proposal and agree with @tabatkins' idea of incorporating this into the existing syntax. So this would then look like

content: src("icon.svg", param(currentcolor))
  1. There should be a way to trigger this via url as well, so you can use it in <img src="icon.svg">.

Agreed but this seems like a whole different feature (and far more complicated to define in a way that doesn't clash with existing params).

The spec. already describes a way to do that for custom properties. Based on that, this would be expressible via

<img src="icon.svg#param(currentcolor)" alt="">

The main motivation for this proposal was for it to be a quick win, something that can be implemented more easily than the full-blown params proposal. As long as tying it to the params proposal doesn't negate that, I think it’s a good idea to integrate them more closely to avoid the cognitive overhead on users of having to learn two distinct syntaxes for highly related things.

While reusing the same function, implementers could still add them independently from the custom properties syntax.

Sebastian

SebastianZ commented 9 months ago

It seems we agree that this should be part of CSS Linked Parameters, so I adjusted the title and labels.

Sebastian

LeaVerou commented 9 months ago

@tabatkins

something that can be implemented more easily than the full-blown params proposal.

All that Params does is set the initial value of variables. It's the exact same feature as what you're proposing, just affecting a different set of things, so they should be equally easy or difficult.

My understanding was that there were some concerns around security about the general case of allowing any page to set arbitrary CSS variables on any resource.

tabatkins commented 9 months ago

To the extent that those concerns are problematic, I think setting the default color and font of arbitrary SVG images would be at least as bad, and likely worse. ^_^

Crissov commented 9 months ago

If I understand @dbaron correctly, Gecko already extends SVG2’s context element to the HTML host element, i.e. <img>, which can be styled with CSS.

The context-fill and context-stroke values are a reference to the paint layers generated for the fill or stroke property, respectively, of the context element of the element being painted. The context element of an element is defined as follows:

  • If the element is within a ‘marker’, (…)
  • If the element is within a sub-tree that is instantiated with a ‘use’ element, then the context element is that ‘use’ element.
  • [Gecko extends this to the HTML host element and both values, context-fill and context-stroke, become the host’s currentColor.]
  • Otherwise, there is no context element.

This means, the SVG code might need to be changed to use the respective values, context-fill and context-stroke, but HTML and CSS would not need to be altered in many cases – whereas both must be specially prepared accordingly for CSS Linked Parameters.

<style>.icon {color: green;}</style>

<img class="icon" src="#icon" />
<img class="icon" src="icon.svg" />
<img class="icon" src="sprites.svg#icon" />

<svg id="icon"><path fill="context-fill" d="M 1744 267, 711 1300, 176 765, 0 941 l 711 711, 1209 -1209 z"/></svg>

<svg><use class="icon" href="#icon"/></svg>
<svg><use class="icon" href="icon.svg"/></svg>
<svg><use class="icon" href="sprites.svg#icon"/></svg>

I didn’t look far enough into the “bug” discussions to understand whether this would also apply to SVGs used from CSS, but I would expect so:

i {color: green;}
i::before {content: url("icon.svg");}
i::after  {content: url("sprites.svg#icon");}

I think this is the proper approach, but I understand that this isn’t viable in many scenarios, because the SVG cannot be changed by the CSS author for some reason – but then param() will likely also fail.

I also wished, SVG and HTML could simply reference a shared CSS stylesheet or reuse some (proposed) Shadow DOM mechanism.

:host(img.icon) path {fill: green;}
img.icon::shadow > svg path {fill: green;}
img.icon >>> path {fill: green;}
img.icon:root(svg) {color: green;}

However, being able to set the initial expansion of predefined keywords from link parameters would be nice as well.

<img class="icon" src="icon.svg#param(context-fill green)"/>
<img class="icon" src="sprites.svg#icon#param(context-fill green)"/><!--?-->
i::before {content: src("icon.svg" param(context-fill green));}
i::after  {content: src("sprites.svg#icon" param(context-fill green));}
faceless2 commented 7 months ago

I really like the idea of integrating this with link-params, but I'm not totally sure I've followed the mechanism that's proposed here.

link-params allow a source document to set a variable in the target document, with the value is passed through to the target document as if the variable were set in a user-agent stylesheet (or any equivalent process resulting in the rules having lower priority than any rules set in the target)

Properties in the source document
------------------
link-parameters: param(--foo x);

Effective new user-agent stylesheet in the target document
------------------
:root { --foo: x }

And if x is missing in the param(), it defaults to the value the same variable in the source document; so var(--foo)

If I understand this proposal, the idea is to extend this so if the parameter name is not a custom-ident, it's parsed as a CSS property name and the default value is the source element's computed value of that property.

Properties in the source document
------------------
font-family: serif
link-parameters: param(font-family);

Effective new user-agent stylesheet in the target document
------------------
:root { font-family: serif }

Is that correct? I like it if so, easy to use and no harder to implement than regular link-params. Two questions: