Selecting matching attribute values in CSS


#1

Hello!

Recently I’ve been controlling the view state of components with attribute values in the top-level element. Here’s a minimal example of the structure I’ve been using:

HTML:

<body data-active-view="1">
   <section data-view="1">1</section>
   <section data-view="2">2</section>
   <section data-view="3">3</section>
</body>

CSS:

[data-view] { display: none }

[data-active-view="1"] [data-view="1"] { display: block }
[data-active-view="2"] [data-view="2"] { display: block }
[data-active-view="3"] [data-view="3"] { display: block }

The only problem is that the CSS displaying logic is static – adding more views would require either adding more CSS rules dynamically, or doing it preemptively by adding an arbitrary amount of rules for more views (for a 4th view, 5th, 6th, etc).

I think a smarter way to do this would be to abstract the value of an attribute into a variable, something like:

[data-view] { display: none }
[data-active-view=@value] [data-view=@value] { display: block }

This syntax would result in writing less CSS, while also explaining the purpose of the CSS logic more clearly.


Playing around with this proposed syntax, you can also do cool things like

  • Using a variable with advanced attribute value selectors
    • [attr~=@value], [attr|=@value], [attr^=@value], etc
  • Using multiple variables in the same selector string
    • [data-active-view=@value][other-attr=@other-val] [data-view=@value][other-attr=@other-val] { display: block }

I haven’t researched too much into the practicality of a syntax like this – so I’m totally open to different proposals :smile:

Let me know what you think!


Update 10/2/16 Here’s a better organized description of the feature and the proposed CSS syntax: github.com/tvler/Matching-Attribute-Values-CSS-Selector/blob/master/explainer.md


#2

The particular way of writing it is debatable, but let’s start more basic - i’m having trouble understanding where variable value would come from?


#3

So in my syntax example:

[data-active-view=@value] [data-view=@value] { display: block }

The variable @value isn’t really being defined anywhere – it’s being used to declare this rule: Any [data-view] child element of [data-active-view], where the data-view attribute value matches the attribute value of data-active-view.

Does that clear it up for you? I understand that it’s a little wonky to use a variable like this with no declaration, it just currently seems like the simplest syntax to reference values within multiple attributes.


#4

AFAICT, the idea is to have a meta selector that contains the same named value multiple times, regardless of the exact specific value.

So, based on the topic starter’s example, the proposed selector would only be triggered for cases when there are two nested elements (a container and a descendant) that have their specified attributes (data-active-view for the container and data-view for the descendant) with the same value.

But this is actually easily solved by using regular classes (i. e. current like in the example below):

[data-view].current {display: block; }
<body data-active-view="1">
    <section data-view="1" class="current">1</section>
    <section data-view="2">2</section>
    <section data-view="3">3</section>
</body>

#5

Yeah this is a great way to handle controlling the current view as well!

I found that is was really beneficial to control the view state from a top level element attribute, instead of using the display logic you wrote up.

Specifically, at my job we needed to control a component’s state while also referencing the currently active view for js logic. I found that retrieving the currently active view simply by doing

activeView = parseInt(parent.getAttribute('data-active-view'), 10)

was a really clean approach, instead of doing something like getting activeView by the .current child index to data-active-view.


Maybe the two display logic techniques could meet somewhere in the middle. I could use a MutationObserver to hook which container has the .current class to any data-active-view parent attribute changes.

Still, this is JS overhead that I think can be simplified into a cleaner CSS-only approach. Thanks for your input!


#6

Since you are using JS anyway, I have no idea why not just use classes. If activating a tab is performed automatically and its logic is somewhat hidden from you, then there should almost certainly be a legit way to define a hook function without any need to use workarounds like MutationObserver.


#7

Yeah, sorry, I’m still having trouble understanding what you’d like to do/why… maybe you could create a codepen or jsbin or something?


#8

Sure. Getting ready for a flight, I’ll write up a quick example later today or tomorrow :thumbsup:


#9

Took me a few reads to get it but I’m pretty sure the idea here is for there to be a placeholder in the selector written 2 or more times. This placeholder can match anything valid there but the rule only matches if all the placeholders have the same value.

Here’s a pen expanding slightly on @ty_ler’s example. We have a grid and want to select every element on the diagonal.

Using the current selectors I’ve given a green border to the matching elements, I need a rule for each matching element.

Using @ty_ler’s proposed selector I would be giving a red background to each matching element. I only need 1 rule no matter how many rows or columns I have.

Finally I’m not sure if this would work as proposed but seems like it should (and if not I’m proposing it would) using @value in nth-child so now I still only need 1 rules but I don’t even have to add the row and column attribute numbers to my HTML (which could be brittle in a dynamic page).

Another use case would be for tabs. A basic approach to tabs involves a click handler on each tab handle, find any panels with an active class, remove that class, then attach the active class to the panel that matches the tab. With this selector you could just have the click handler change an attribute on the container and use a rule like:

.tabs .panel { display: none; }
.tabs[open-tab=@value] .@value { display:block; }

This will display any panel whose class matches the tab container’s open-tab attribute.


#10

@chaoaretasty Thank you so much for writing up the example. Your understanding of my proposal is 100% correct.


@MT Since you are using JS anyway, I have no idea why not just use classes.

I understand this is the standard way to control display state. This work project of mine was actually my first time using a top-level attribute to control an active state, and I was really surprised with how much I enjoyed the development experience.

I found the practice

  • Easy to debug – you just have to change one value to change the display state instead of adding a current class and removing another current class
  • Easy to read – the active state is controlled in one top-level location rather than in multiple elements
  • Easy to work with in JS – changing a view state is just changing a single attribute rather than adding a current class and removing another current class, retrieving the active state value is as easy as getting an attribute value

#11

I too frequently use the concept of having a data-attribute on a container element identify the visible/primary/active content. It’s basically the idea of aria-activedescendant. The primary problem is identifying the target element from within CSS.

You currently have two options: verbose CSS selectors, or some script mutating the target elements so they become identifiable in CSS.

  • Adding classes like .current (as explained by @MT) is cumbersome because you always need to traverse the DOM to find the elements to add the class to, as well as the element(s) to remove the class from. Especially in “MVC” situations this may be a pain, because the root element (containing the aria-activedescendant or custom analog) and the referenced items may be handled by different control units.
  • The verbose CSS selectors approach gets by without the “heavier JS” implication, but comes at the cost of having to know the available references before hand. Now you’re maintaining relationships in HTML/DOM, JS and CSS.

In my mind all of these references are nothing but a custom implementations of :target, but instead of being backed by the fragment, they use DOM attributes for target selection.

So what we’re looking for is a way to simplify

[aria-activedescendant="alpha"] #alpha,
[aria-activedescendant="bravo"] #bravo,
[aria-activedescendant="charlie"] #charlie {
  /* styles */
}

[aria-activedescendant="alpha"] [id="alpha"],
[aria-activedescendant="bravo"] [id="bravo"],
[aria-activedescendant="charlie"] [id="charlie"] {
  /* styles */
}

[data-gustav="alpha"] [data-by-gustav="alpha"],
[data-gustav="bravo"] [data-by-gustav="bravo"],
[data-gustav="charlie"] [data-by-gustav="charlie"] {
  /* styles */
}

If you deconstruct these selectors, you have a local-selector and local-attribute as well as an ancestor-selector and ancestor-attribute that need to match. Not considering explicit element selection, you could describe this with a pseudo-class accepting two attribute names :matches-ancestor-attribute(local-attribute-name, ancestor-attribute-name), where ancestor-attribute-name is read off the first ancestor with that attribute being set on:


.selector:matches-ancestor-attribute(id, aria-activedescendant) {
  /* styles */
}

.selector:matches-ancestor-attribute(data-by-gustav, data-gustav) {
  /* styles */
}

An alternate, more powerful approach could work along the lines of providing the ability to use CSS Custom Properties for this.

.ancestor-element {
  --selected-item: attr(aria-activedescendant);
}

.potential-target[id=var(--selected-item)] {
  /* styles */
}

#12

I think that the CSS variable approach is very interesting! I don’t believe variables can be declared or used outside of a selector block as of now (maybe I’m wrong about this…?), and I could imagine there may need to be some syntax to wrap a variable within a selection string so it wouldn’t be read just as #var(--selected-item) verbatim.

Building off @rodneyrehm’s example, maybe something like

.potential-target[id=@var(--selected-item)] {}

Thanks for your input Rodney :slight_smile:


#13

This seems unlikely because ‘variables’ are custom properties and this creates the famous circularity problem of the same thing being able to both get and set… In other words, you could have two rules which both select based on the value of custom property “–a” and in the rule, set custom property “–a” to something else which causes the other rule to take effect and potentially set custom property “–a” back to the thing that set the first rule into effect and so on, forever.


#14

This is an interesting use case too, thanks for adding it. There may be something fundamental missing here, this definitely seems worth some further discussion. I’m not sure how to handle this but maybe a good thing to do would be to see if we can come up with a very succinct explainer of the problem and some example use cases. It seems to me this would be better done in a github repo.


#15

Do you mean you still don’t understand what the proposal is about?


#16

no, I just mean a good next step would be to explain the problem simply as possible, now that people have had some practice - in a repo, listing the use cases we have (not necessarily a proposed solution wrt how) so that we can get more people to consider the problem space. . From there people can open issues/add use cases, etc. Just a thought. The longer this particular thread gets, the less likely I think someone will take the time to read through all the attempts to explain/questions/etc and participate, that’s all.


#17

I’d be happy to organize a repo with a more organized proposal. Would it be best to set one up through my github account, or would it be created in github.com/wicg?


#18

yes, just start with yours… if we get that far it can migrate, that’s done that all the time.


#19

On the same topic of non-script-dependent, non-target-based element modality, it’d be nice if we had a less hacky way to have elements that toggle another element’s modality without scripting than the somewhat-accessibility-hostile hidden-checkbox approach detailed by Paul Lewis in his second Chrome Dev Summit developer diary video yesterday.


#20

Hey everyone, I wrote up an explainer doc for the feature:

While writing it up I actually came up with another CSS feature that would not only make this feature possible, but make a bunch of other things to do in CSS possible as well: exposing the computed value of a css variable at a specified scope in a selector string.

Here’s an example of the new feature being used to accomplish the behavior of the first example I gave in this thread:

[data-active-view]:var(--active-view) [data-view=var(--active-view)] {
  display: block;
}

To learn more about this, read the Solution & Final Solution sections in the explainer doc. It’s based off of @rodneyrehm’s syntax proposal but addresses some flaws I think I found.

So, do you think I should continue my feature proposal in this thread, or create a new thread now that this feature addresses more use cases than just selecting matching attribute values?