Skip to content

[css-values-5] Proposal for a new matches() CSS Function #10594

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

Open
brandonmcconnell opened this issue Jul 18, 2024 · 2 comments
Open

[css-values-5] Proposal for a new matches() CSS Function #10594

brandonmcconnell opened this issue Jul 18, 2024 · 2 comments

Comments

@brandonmcconnell
Copy link

brandonmcconnell commented Jul 18, 2024

Abstract

The matches() function is proposed to enhance the conditional application of styles in CSS by checking if the current element matches a specified selector. This function complements the recently accepted new if() function, providing a more semantic and readable way to handle conditional logic within CSS properties.

Motivation

The current method of conditionally applying styles in CSS often involves the use of arguably complex techniques like emulating boolean values using integers (0 and 1) or toggles using empty values (The -​-var: ; hack...), both of which can become complex and less readable.

The introduction of if() allows for clearer conditionality using boolean logic. However, the need to conditionally style elements based on their selector remains verbose and needs and is only really doable via addtl rules/selectors in the cascade, even if it would be simpler inline in some cases. The matches() function aims to address this by allowing a direct approach to conditionally setting styles based on selector matches.

This would also bring greater parity with the related JS Element.prototype.matches method.

Syntax

matches(selector)
  • selector: A CSS selector to check against the current element.

Usage

The matches() function can be used inside any CSS property value expression where conditional styles are necessary based on whether the current element matches a specified selector.

Example 1: Basic example

.item {
  --symbol: if(matches(.favorite) ? "🧡" : "🩶");
  &::before {
    content: var(--symbol);
  }
}

In this example, the --symbol custom property is conditionally set to different emoji characters based on whether the .item element has the .favorite class.

Comparison to existing methods

Example 2.1: Without if() and matches()

.item {
  --symbol: "🩶";
  &::before {
    content: var(--symbol);
  }
  &.favorite {
    --symbol: "🧡";
  }
}

This traditional approach involves redundant code as it sets the same property (--symbol) in multiple places.

Example 2.2: Using if() and matches()

.item {
  --symbol: if(matches(.favorite) ? "🧡" : "🩶");
  &::before {
    content: var(--symbol);
  }
}

This approach reduces redundancy and centralizes the logic for setting the --symbol property.

A case for boolean expressions outside if()

I've opened a separate proposal (#10593) focused on supporting a new "boolean" custom property type, which could hypothetically be valid and type-safe even outside of if(). If that's accepted, an example using a math-based switch like the one below could be simplified to use boolean logic.

Example 3.1: Using a math-based switch

This example uses a math-based switch to zero out values when a condition is not met.

label {
  --selected: 0;
  &:has(:checked) { --selected: 1; }
  grid-template-columns: auto calc(var(--selected) * 20px);
  svg { opacity: var(--selected); }
}

Example 3.2: Using matches() and if()

This moves the conditions inline, but they're a bit complex, so it's not ideal that we have to repeat the conditions in both values.

label {
  grid-template-columns: auto if(matches(:has(:checked)) ? 20px : 0);
  svg { opacity: if(matches(:has(:checked)) ? 1 : 0); }
}

Example 3.3: Using matches() and if()

Using reusable boolean values can offload some of this complexity.

label {
  --selected: matches(:has(:checked));
  grid-template-columns: auto if(var(--selected) ? 20px : 0);
  svg { opacity: if(var(--selected) ? 1 : 0); }
}
@brandonmcconnell brandonmcconnell changed the title [css-values-5] Proposal for a new match() CSS Function [css-values-5] Proposal for a new matches() CSS Function Jul 18, 2024
@romainmenke
Copy link
Member

romainmenke commented Jul 19, 2024

Wouldn't if inherit like light-dark with custom properties?

see : https://codepen.io/romainmenke/pen/wvLGwyb

<div id=a>
  hello
  <div id=b>
    world
  </div>
</div>
:root {
  --fg: light-dark(black, white);
  --bg: light-dark(white, black);
}

#a {
  color-scheme: dark;
  color: var(--fg);
  background-color: var(--bg);
}

#b {
  color-scheme: light;
  color: var(--fg);
  background-color: var(--bg);
}

In this example --fg and --bg are not "evaluated" to specific colors based on the color scheme of :root. Instead the light-dark() functions are inherited and evaluated on specific elements against their respective color scheme.

So in your first examples matches() would never work because those pseudo elements would never have the .favorite class. Right?

I think you would need to write:

.item {
  --symbol: if(matches(.favorite::before) ? "🧡" : "🩶");
  &::before {
    content: var(--symbol);
  }
}

I think authors will often be surprised by such behavior because you need to consider the outer selector, how values cascade and inherit and how both will affect the inner selector.

Is the added complexity worth it, considering that we can already style elements by matched selector? On the other hand, exactly this complexity and behavior could be the feature here.

@josephrocca
Copy link

josephrocca commented Sep 7, 2024

(Please feel free to just read the TL;DR at the end.)

(Context) In search of light-dark()-level simplicity for hovered elements

I was about to create an issue in this repo asking whether something like light-dark() is possible, but for :hovered elements. I love the simplicity of light-dark(), and I've always wanted to be able to define hover colors in inline styles. Something like this:

<div style="color: hover-else(blue, black)">this text turns blue when hovered</div>

While researching before creating the issue, I came across the if/matches proposals. If I understand correctly, it would look something like this:

<div style="color: if(matches(:hover) blue ? black)">this text turns blue when hovered</div>

(Question) Can this proposal be adapted to reduce nesting for simple cases like this, or should it be a separate proposal?

For a common case like hover-based color changes, the degree of nesting and syntax required to achieve this using if/matches seems a bit too far. I guess I see a few possibilities here:

  1. A separate proposal for a helper function like hover-else.
  2. Cover the common "if , valueX, else valueY" case without requiring two levels of nesting, at least. Something like color: cond(:hover ? blue : black).
  3. Allow matches to return values in place of true and false if they're given as two extra arguments - e.g. color: matches(:hover, blue, black).
  4. Forsake the "simple/common things should be simple" principle in favor of the "better to have fewer, simpler, composable building blocks" principle, and just make devs write color: if(matches(:hover) blue ? black).

My preference is 3, and I'm wondering what others think about this? I.e.:

<div style="color: matches(:hover, blue, black)">this text turns blue when hovered</div>

TL;DR: Can we allow matches to return values in place of true and false if they're given as two extra arguments - e.g. color: matches(:hover, blue, black) would return blue in place of true and black in place of false?

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

4 participants