w3c / csswg-drafts

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

[selectors] Parameterized selectors #10567

Open matthewp opened 1 month ago

matthewp commented 1 month ago

CSS is great for filtering content. This lets you keep your DOM mostly static and change what is visible by toggling attributes. However, this breaks down with dynamic content.

Take this example:

<input value="taco">

<ul>
  <li data-name="crunchy tacos">crunchy tacos</li>
  <li data-name="meat pie">meat pie</li>
</ul>

I would like to highlight the li that matches the input value. To achieve this today you need to write JavaScript to do the filtering. Usually this means rerendering the entire list to either remove the lis that don't match (which is what almost everyone does), or perhaps toggling attributes onto the matching one. In either case, this is an expensive and code-heavy way to do filtering.

If CSS selectors could be used as parameters you could achieve the filtering without manipulating the list items. Here's an example of what I would like to do: (note that i know this is horrible and invalid syntax, it's only meant to communicate the goal)

input$val=attr(value) + ul [data-name^=$val] {
  color: tomato;
}
noamr commented 1 month ago

I've somewhat recently proposed something similar in the context of view transitions: https://github.com/w3c/csswg-drafts/issues/8320#issuecomment-1877522852

/cc @mirisuzanne

matthewp commented 1 month ago

That's much better, rewriting my thing using that syntax (if i understand it):

input[:value] + ul [data-name^=[:value]] {
  color: tomato;
}
ChrisShank commented 1 month ago

I've also come across wanting something similar with expanding/collapsing groupings in HTML tables. Basically if a grouped row is collapsed then we want to hid all of the visually nested rows (semantically the rows in the HTML table are flat, so we want to hide the next n siblings). And I wish I could have just written a selector like this:

table tr[aria-expanded="false"][:path] ~ tr[path^=[:path]] {
  display: none;
}
romainmenke commented 1 month ago

CSS is great for filtering content.

I would challenge this. This pattern isn't good for accessibility.

Using this pattern as the main motivation for new syntax and features might not be ideal.

Do you have other use cases?

Something dynamic that is hard to do today and wouldn't negatively affect accessibility if done purely with CSS?

matthewp commented 1 month ago

Hi @romainmenke, can you explain why this pattern is bad for accessibility? display: none hides the content from screen readers.

romainmenke commented 1 month ago

I think I was conflating two things 🤔 I thought that such toggling with CSS would not be good for accessibility. But as long as you set the right aria attributes screenreaders will advertise changes.

I wasn't conserned about hiding from screen readers, more about advertising changes to the content.

I think I am confusing this with the old-style checkbox hacks where only the label is visible and the input itself is hidden.

Comment above edited.

mirisuzanne commented 1 month ago

I agree it would be useful (seemly in several situations). With @noamr's original proposal, there was a more clear distinction between the value we're grabbing (a selector syntax) and the place we're using it (in a declaration value). Once you put those both into selectors, using the same syntax, it's hard to know which is defining the parameter and which is accessing it.

I think there's been progress on disentangling the attr() proposal - removing the problematic bits, so that it can move forward. This feature is a bit different, but they work in a similar space, so there may be some crossover.

For this use-case specifically, the proposal only seems to address exact match. Is that good enough to solve most use-cases here?

Those are my initial reactions. I'm not an editor on the selectors spec, so it would be good to get input from @tabatkins or @fantasai as well.

Alohci commented 1 month ago

Selecting on an input :value pseudo-class would enable CSS keyloggers. I don't think that's a good idea.

noamr commented 1 month ago

Selecting on an input :value pseudo-class would enable CSS keyloggers. I don't think that's a good idea.

It just selects the value attribute, not the actual typed value

brunostasse commented 1 month ago

One use case I can see for this feature is styling elements belonging to a component instance based on attributes set on the component instance's root element when several instances are intertwined.

The @scope feature, which has been partially thought to style component's elements, doesn't help in such cases, because it always ties elements to the closest component instance's root element. It doesn't allow to style elements scattered across several instances of the same component.

For example, if a button belonging to a Dialog component is nested in two Dialog instance's root element, but belongs to the outer one, then the only way to style the button based on a state/variant/modifier of its component instance is to add that state/variant/modifier to the button element, and all other elements belonging to that Dialog instance. Like so:

<div class="Dialog-root type-alert"> <!-- dialog-1 root element -->
   <div class="Dialog-root type-warning"> <!-- dialog-2 root element -->
      <button class="Dialog-button type-alert">Close dialog-1</button>
      <button class="Dialog-button type-warning">Close dialog-2</button>
   </div>
</div>
.Dialog-root.type-alert {
   background-color: red;
}
.Dialog-button.type-alert {
   color: red;
}

Whereas with this feature, all elements belonging to one component instance can be tied together with an id, like so:

<div class="Dialog-root type-alert" data-cid="dialog-1">
   <div class="Dialog-root type-warning" data-cid="dialog-2">
      <button class="Dialog-button" data-cid="dialog-1">Close dialog-1</button>
      <button class="Dialog-button" data-cid="dialog-2">Close dialog-2</button>
   </div>
</div>
.Dialog-root[:data-cid].type-alert {
   background-color: red;

   .Dialog-button[data-cid=[:data-cid]] {
      color: red;
   }
}

This reduces the number of DOM operations needed to update states/variants/modifiers on a component, as it only needs to be done on its root element rather than on all affected elements.

matthewp commented 1 month ago

For this use-case specifically, the proposal only seems to address exact match. Is that good enough to solve most use-cases here?

I don't think I follow, what do you mean by exact match? We have non-exact matches through the attribute selectors ^=, *=, etc. I think you probably mean something different though.


One other use-case would be something like ranges (is that what you mean by non-exact)? For example you want to filter the selected items whose price is between 20-100. I don't know how you could express something like that though, it would mean a new type of selector I would imagine.

DaniGuardiola commented 1 month ago

My use case is keeping long selectors DRY, for example:

:is(:hover,:focus-visible,[data-focus-visible]):not(disabled,[aria-disabled],[data-disabled])

This is a selector that targets elements that are being hovered or focused through the keyboard, and are not disabled. This is common, since many times the styles will be the same for these states.

Note that it includes selectors that could be set by JavaScript libraries such as Ariakit or Radix, which typically "extend" the web platform with accessible UI primitives. These libraries can't trigger native selectors like :focus-visible and, therefore, need to signal them through other means like data attributes. Additionally, they might need to communicate a disabled state, but the disabled attribute is invalid in most elements, and therefore either aria-disabled or a data attribute must be set.

I would like to be able to define this once, and use it everywhere to target these states consistently. My current solution is a custom PostCSS plugin that adds states through :is-targeted and similar non-standard pseudo selectors, which get transformed into their long variants.

While the approach works, it results in a hard to parse output which is also larger. The alternative (writing these by hand) can result in significant usability or accessibility problems, as it's fairly easy to miss an important part of a selector or forget to update existing ones with important changes (e.g. if a library is upgraded).

I hope I explained my use-case clearly enough! Would love to see this land.

romainmenke commented 1 month ago

use case is keeping long selectors DRY

Aren't custom selectors a better proposal for that?

@custom-selector :--on-interaction :is(:hover,:focus-visible,[data-focus-visible]):not(disabled,[aria-disabled],[data-disabled]);

.button:--on-interaction { color: green; }

or even:

```css
@custom-selector :--disabled :is(disabled,[aria-disabled],[data-disabled]);
@custom-selector :--focus-visible :is(:focus-visible,[data-focus-visible]);
@custom-selector :--on-interaction :is(:hover,:--focus-visible):not(:--disabled);

.button:--on-interaction { color: green; }
DaniGuardiola commented 1 month ago

@romainmenke yes, this exactly how I envision it (specific syntax aside). Is there already a proposal for this?

romainmenke commented 1 month ago

This is the relevant specification: https://drafts.csswg.org/css-extensions/#custom-selectors

mirisuzanne commented 1 month ago

I don't think I follow, what do you mean by exact match? We have non-exact matches through the attribute selectors ^=, *=, etc. I think you probably mean something different though.

@matthewp yeah, that provides some flexibility. I don't know that I had a specific use-case in mind, just noting the limitation. I think your range idea is a good example that would require more. I wasn't intending it as a blocker - just noting that 'search using selectors' use-case could open a wide range of more regex-like pattern-matching needs.