web-platform-tests / interop

web-platform-tests Interop project
https://wpt.fyi/interop
281 stars 28 forks source link

[css-values-5] `attr()` support extended capabilities #521

Closed brandonmcconnell closed 7 months ago

brandonmcconnell commented 11 months ago

Description

The attr() CSS function is used to retrieve the value of an attribute of the selected element and use it in the stylesheet. It can also be used on pseudo-elements, in which case the value of the attribute on the pseudo-element's originating element is returned. (MDN)

Specification

CSS Values and Units Module Level 5
# attr-notation

Open Issues

Tests

The below list of tests was generated using wpt-find (wpt-find -mlc 'attr(')

Current Implementations

Standards Positions

Browser bug reports

Developer discussions

Some posts in support of attr():

X/Twitter

StackOverflow

If you search X/Twitter for "CSS attr" you will see plenty of posts from people excited about and/or curious about the future features of CSS attr(). You'd also see many posts echoing one opinion calling the attr() useless, especially in comparison to CSS custom properties.

I'd like to disagree with that sentiment, but… he's right. As the attr() function stands today, attr() is really only useful when trying to pull plain text from attributes to display using a pseudo-element.

It's mostly useless, but it doesn't have to be.

With these newer features, attr() would be exceedingly useful, not taking the place of CSS custom properties by any means but rather bridging the gap between markup values and CSS values. This would stand as a very productive separation of concerns.

Polls & Surveys

No response

Existing Usage

No response

Workarounds

The primary workaround for this is to use CSS custom properties, but this has several drawbacks and only lives in the markup in the form of inline styles. One of the greatest advantages of attr() is the ability to utilize the values set on the markup.

To achieve this with CSS custom properties, one would need to apply those values twice, once to the attributes for actual markup functionality and again as a CSS custom property for CSS usage. Example:

<!-- tightly coupled redundancies in the attributes vs. inline styles -->
<progress min="0" max="100" value="50" style="--min: 0; --max: 100; --value: 50;"></progress>

<style>
  progress {
    --percentage: calc((var(--value) - var(--min)) / (var(--max) - var(--min)) * 100%);
    background: linear-gradient(to right, green var(--percentage), white var(--percentage));
  }
</style>

With attr(), we can achieve this more concisely, using attribute values directly:

<!-- here, the styles will inherit from the attributes dynamically, so no need to set the explicit values twice -->
<progress min="0" max="100" value="50"></progress>

<style>
  progress {
    /* I'm using `attr()` directly in this calculation, but these values could also be abstracted to CSS custom properties for ease of reuse */
    --percentage: calc((attr(value number) - attr(min number)) / (attr(max number) - attr(min number)) * 100%);
    background: linear-gradient(to right, green var(--percentage), white var(--percentage));
  }
</style>

Accessibility Impact

Tightly coupled code can lead to inconsistencies due to human/developer-error when updates are made. The DRY coding principle would reasonably apply here.

Privacy Impact

This issue focuses on security concerns using attr(), but there are already good ideas re how to circumvent those issues, the primary of which would be whitelisting: https://github.com/w3c/csswg-drafts/issues/5092

Other

No response

webstrand commented 11 months ago

I like to use display: grid for <table> markup. Being able to write generic css that handles most of the built-in table attributes would be nice, e.g. rowspan colspan ...

thebabydino commented 11 months ago

Here's another use case: images with a spaced out gradient border that looks extracted from the image.

images surrounded by a gap then by a gradient border obtained from the image

The markup for an image would be just

<figure>
    <img src='meow.jpg' alt='fluffy black cat'/>
</figure>

Then in the CSS, this would do the trick for the image with the spaced out "gradient" (actually very blurred) border:

img {
    border: solid 1em;
    border-image: filter(src(attr(src)), blur(99px)) 10%;
    padding: 1em; /* space between image and border */
    width: 12em;
    aspect-ratio: 1;
    object-fit: cover
}

No browser currently supports this and it's not just because of attr(). src() doesn't seem to be in any browser at the moment, while filter() is only supported in Safari (has been supported there for over 8 years at this point, but no other browser followed).

The current cross-browser solution feels needlessly complicated.

First, when I generate the HTML, add a --url custom property that uses the same image url from the src attribute.:

- let url = 'meow.jpg';

figure
    img(src=url style=`--url: url(${url})` alt='fluffy black cat')

Then in the CSS, I need all of this:

figure {
    display: grid;
    width: 16em;

    img, &::after {
        grid-area: 1/ 1;
        border: solid $b hsla(0, 0%, 0%, .001)
    }

    img {
        box-sizing: border-box;
        border-image: var(--url) 10%;
        padding: $b;
        width:100%;
        aspect-ratio: 1;
        object-fit: cover
    }

    &::after {
        backdrop-filter: blur(99px);
        mask: 
            linear-gradient(red 0 0) padding-box exclude, 
            linear-gradient(red 0 0);
        content: ''
    }
}

And this doesn't even include the -webkit--prefixed version for mask, which is still needed at this point (live demo).


With attr(), we can achieve this more concisely, using attribute values directly:

<!-- here, the styles will inherit from the attributes dynamically, so no need to set the explicit values twice -->
<progress min="0" max="100" value="50"></progress>

<style>
  progress {
    /* I'm using `attr()` directly in this calculation, but these values could also be abstracted to CSS custom properties for ease of reuse */
    --percentage: calc((attr(value number) - attr(min number)) / (attr(max number) - attr(min number)) * 100%);
    background: linear-gradient(to right, green var(--percentage), white var(--percentage));
  }
</style>

I'm intrigued! This looks very good in theory and I'd love to be able to use something like this for inputs! For example, for input[type=range], this would eliminate the need for JS as we wouldn't need to rely on a JS-updated custom property for the value, like I'm doing in this demo.

But one thing that confuses me is that I don't see the value attribute for such an input changing in DevTools as I drag the thumb. The actual value (that I update the --val custom property with) does change, but not the number value in the value attribute. So would attr(value) actually work, or would it just be always the same value set initially in the value attribute?

DevTools screenshot. Shows how after dragging the slider thumb, the --val custom property in the style attribute has changed value, but the value attribute has kept the initial value it was set to.

If attr(value) could work in calc() and could get updated on dragging the thumb, that would be wonderful. Because I currently use the --val attribute to compute quite a few things, for example the position of a tooltip value display and the number value shown in that tooltip. And since --val can only be updated via JS, no JS means no tooltip value display.

brandonmcconnell commented 11 months ago

@thebabydino Solid feedback all around. Examples like that greatly help to express this feature like this. Thank you!!

Re the value attribute updating, I've run into that as well a number if times and have a couple other proposals to address that—

  1. [css-values-5] value() function

    This proposal introduces a CSS function for incorporating the live value of an input, exactly as you've put it here

  2. [css-properties-values-api] add support for scope-global variables

    This proposal introduces a completely new way to work with CSS custom properties — globally.

    (expand/collapse explainer)
    Hypothetically, you could use `@property` to set up a custom property with a new `global` option on its `@property` config set to `true`. When the custom property is set up this way, it only ever has one value — that with the highest specificity. This way, we can set global values using states such as a checkbox or an input value and access it anywhere else in that scope. For example, you could create a dark mode toggle checkbox somewhere on the page, and without needing to resort to some top-level `:has()` (not inherently bad, but an alternative), you could have that checkbox's `checked` state update the global value naturally, like this: ```css @property --color-bg { syntax: ""; inherits: true; initial-value: white; /* ⬜️ */ global: true; } @property --color-text { syntax: ""; inherits: true; initial-value: black; /* ⬛️ */ global: true; } #darkmode:checked { --color-bg: black; /* ⬛️ */ --color-text: white; /* ⬜️ */ } #page { background-color: var(--color-bg); /* ⬛️ */ color: var(--color-text); /* ⬜️ */ } ```

    A feature like this would be essential for exposing attributes as well as the value property of descendant elements that cannot otherwise expose them upwards. I'm considering this feature a dependency of my other value() proposal.

I would certainly appreciate any support you'd show to either of those proposals as well. Thank you 🙂

OnurGumus commented 11 months ago

In addition I would add, inline styles aren't very good for security and CSP perspective.

webstrand commented 11 months ago

@thebabydino

This looks very good in theory and I'd love to be able to use something like this for inputs! For example, for input[type=range]

This was my first thought, too. Range is a pain to style, currently: https://codepen.io/webstrand/pen/bGLXdPR

So would attr(value) actually work, or would it just be always the same value set initially in the value attribute?

Traditionally, <input> elements don't update their attribute when their value property changes. Changing this behavior would be a breaking change as there's quite a lot of things that reuse inputEl.getAttribute("value") to restore fields to their initial state. For instance <button type="reset"> looks at the attribute value to determine what to reset to.

I'm not sure I'd be a fan of adding special cases for attr(value) though.

brandonmcconnell commented 11 months ago

@webstrand I agree; I don't think we should introduce any special cases using attr() to retrieve dynamic values like the live value prop of an input.

If you're interested, take a peek at the other two proposals I mentioned, namely this one: [css-values-5] value() function

Re the `value` attribute updating, I've run into that as well a number if times and have a couple other proposals to address that— 1. [[css-values-5] `value()` function](https://github.com/w3c/csswg-drafts/issues/7869) This proposal introduces a CSS function for incorporating the live value of an input, exactly as you've put it here 2. [[css-properties-values-api] add support for scope-global variables](https://github.com/w3c/csswg-drafts/issues/7866) This proposal introduces a completely new way to work with CSS custom properties — globally.
(expand/collapse explainer)
Hypothetically, you could use `@property` to set up a custom property with a new `global` option on its `@property` config set to `true`. When the custom property is set up this way, it only ever has one value — that with the highest specificity. This way, we can set global values using states such as a checkbox or an input value and access it anywhere else in that scope. For example, you could create a dark mode toggle checkbox somewhere on the page, and without needing to resort to some top-level `:has()` (not inherently bad, but an alternative), you could have that checkbox's `checked` state update the global value naturally, like this: ```css @property --color-bg { syntax: ""; inherits: true; initial-value: white; /* ⬜️ */ global: true; } @property --color-text { syntax: ""; inherits: true; initial-value: black; /* ⬛️ */ global: true; } #darkmode:checked { --color-bg: black; /* ⬛️ */ --color-text: white; /* ⬜️ */ } #page { background-color: var(--color-bg); /* ⬛️ */ color: var(--color-text); /* ⬜️ */ } ```
A feature like this would be essential for exposing attributes as well as the `value` property of descendant elements that cannot otherwise expose them upwards. I'm considering this feature a dependency of my other `value()` proposal.
jacobrask commented 11 months ago

Another concrete example, an icon component that should be sized in rems to scale with the text, and also use mask-image to allow it to inherit the text color, so dual potential usage of attr in one class. Currently we do something like this

.icon {
  width: var(--_height);
  height: var(--_height);

  &[height='64'] {
    --_height: 4rem;
  }
  &[height='48'] {
    --_height: 3rem;
  }
  &[height='40'] {
    --_height: 2.5rem;
  }
  &[height='32'] {
    --_height: 2rem;
  }
  &[height='24'] {
    --_height: 1.5rem;
  }
  &[height='16'] {
    --_height: 1rem;
  }
  background-color: currentColor;
  mask-image: var(--icon-url);
}
brandonmcconnell commented 11 months ago

@jacobrask Great example. I've done something similar for custom progress bars:

<div class="progress-wrapper">
  <progress min=0 max=100 value=75></progress>
</div>
progress[min][max][value] {
  .progress-wrapper:has(&) {
    --percentage: calc((var(--value) - var(--min)) / (var(--max) - var(--min)) * 100%);
    height: 100vh;
    width: 100vw;
    background: linear-gradient(to right, red var(--percentage), white var(--percentage));
  }
  .progress-wrapper:has(&) & {
    display: none;
  }

  /* min */
  .progress-wrapper:has(&[min="0"]) { --min: 0; } /* ... */
  .progress-wrapper:has(&[min="100"]) { --min: 100; }

  /* max */
  .progress-wrapper:has(&[max="0"]) { --max: 0; } /* ... */
  .progress-wrapper:has(&[max="100"]) { --max: 100; }

  /* value */
  .progress-wrapper:has(&[value="0"]) { --value: 0; } /* ... */
  .progress-wrapper:has(&[value="100"]) { --value: 100; }
}

CodePen demo

bkardell commented 7 months ago

Thank you for proposing attr() support extended capabilities for inclusion in Interop 2024.

We wanted to let you know that this proposal was not selected to be part of Interop this year.

As of the deadline, the specifications for attr() support extended capabilities were not yet complete enough to allow interoperable implementations. To make progress on this area in the future, it will first be necessary to ensure that the feature has a clear specification in a standards track document.

For an overview of our process, see proposal selection. Thank you again for contributing to Interop 2024!

Posted on behalf of the Interop team.

brandonmcconnell commented 7 months ago

@bkardell What features from the official specification are not complete enough to move forward with this feature? The spec is complete and has been for years.

This feature has been suggested for the past few consecutive years via interop and otherwise.

More notably, this issue garnered more attention and praise and engagement than any other interop issue proposed, which I believe should be weighed heavily when considering proposed features for interop.

One of the primary functions of proposing issues for interop is to give the community the ability to vote on which issues they desire to see implemented in the web platform most. The results clearly show that this is a top priority if not THE top priority.

foolip commented 7 months ago

@brandonmcconnell you're right that there's a lot of developer demand for this, that is very clear. The issue with the spec isn't really that it's "not yet complete enough to allow interoperable implementations", but that there are security issues that at least the Chrome team believes are serious enough to need solving before shipping. That is the topic of https://github.com/w3c/csswg-drafts/issues/5092, and we now have a sketch of a solution that might work. I am going to follow up on that work so that the security issues can be resolved. (I did take a look while we were evaluating proposals too, but there just wasn't enough time to fix it there and then.)

brandonmcconnell commented 4 months ago

@foolip (cc @tabatkins) Given the recent progress and resolutions in https://github.com/w3c/csswg-drafts/issues/5092, and the exceeding demand on this interop proposal the past two consecutive years (#86 & #521), could we reconsider including it in this year's interop effort, as the blocker is now removed?

bkardell commented 4 months ago

We can't, as part of Interop 2024. Interop choices and tests are carefully chosen so that we have measurable goals that don't significantly change during the year... But don't be too disheartened by that answer: It doesn't mean teams can't all prioritize it independently -- in my opinion, another benefit of the Interop project is just that it gets us collectively thinking/talking about priorities... So, it's totally possible if the situation regarding specs is resolved, that it might get interoperable support in the same year without actually being a part of the Interop project.

brandonmcconnell commented 4 months ago

@bkardell Thanks, that's understandable 🙂