CSS selectors: Ability to match against attribute name, not just attribute value

selectors
Tags: #<Tag:0x00007faa9fb4af90>

#1

For example, we can match value of an attribute given we know the entire attribute name. For example, if I know there are elements with the foo attribute, then I can find elements that have a foo attribute starting with bar:

<style>
  [foo|=bar] { /* ... */ }
</style>

But what if I want to find element that have attribute starting with some, so that I can match two of the following elements:

<div something="foo"></div> <!-- select this -->
<div some="bar"></div> <!-- select this -->
<div awesome="bar"></div> <!-- but not this -->

I would like to select both divs with the attributes starting with some, but there’s no way to specify this.

Assume that we don’t know what the attribute values will be, they might be different depending on what the user is specifying via the attribute.

It would be neat if there was a way to match expressions against attribute names, not just values. Unless I missed it, I haven’t seen any way to do that, but I think it is a legitimate desire because all of HTML is just text, so why not be able to match more of it, not just attribute values?

One thing that Angular does is autogenerate attribute names from CSS classes, for example, something like compiling from

<style>
  .component-name { /* ... */ }
</style>
<div class="component-name"></div>

to

<style>
  [component-name-1234] { /* ... */ }
</style>
<div component-name-1234></div>

where 1234 is a collision-free generated identifier to ensure that the style matching is unique.

If you know the component’s name is component-name, it is literally impossible for a parent component to style a child component because it is impossible to guess what the random ID is.

However, if attribute names were matchable against patterns, then a parent component could override child styles.

When using JSS, it mostly provides encapsulation by generating class names similarly to Angular, but it applies to class names (which are attribute values) rather than attribute names, so it is possible to override styling if absolutely needed.

For example, here’s how class names are generated with JSS:

const {classes} = jss.createStyleSheet({
  fancyButton: {
    background: "blue";
  }
}).attach()

document.querySelector('.some-container').innerHTML = `
  <button class="${classes.fancyButton}"></button>
`

then the class name given to the <button> matches the generated clash-free class name that is inside the <style> that is appended into the <head> by JSS.

However, we can still override the style if necessary because we have the power of matching patterns against attribute values, and given that I know fancyButton is converted into something like fancy-button-1234 in the output <style>, I can write

<style>
  [class|=fancy-button] { /* ... */ }
</style>

and I can override styles in there without having to know what the generated ID of the class name is.

There’s also other possible uses. Suppose I want to style all elements that have on* attributes to define event handlers. If we had attribute name matching, then it would be easy. Suppose attribute name matching works similarly to value matching:

<style>
  [^=on] { outlined: 1px solid red }
</style>

The ^= symbol is leading, which means it is matching the following attribute name, and now I’ve outlined all elements that have inline event handlers.

It would be possible to also match the attribute value besides matching the attribute name. For example, I want to style all elements that have an inline event handler that calls the foo() function

<style>
  [^=on^="foo("] { outlined: 1px solid red }
</style>

And boom, they are all outlined!

Maybe ^=on is not specific enough, or clashes with something else; we could select elements with mouse handlers of any sort:

<style>
  [^=onmouse] { outlined: 1px solid red }
</style>

Let’s pick any element with data attributes:

<style>
  [^="data-"] { outlined: 1px solid red }
</style>

Etc, I’m sure there’d be more interesting uses.


#2

A related proposal and discussion in www-style@w3.org mailing list (2016-04-05):

[css-selectors] Tagname based substrings


#3

So there are a few different tools that would let you make use of functionality like this from CSS by leveraging JavaScript, or XPath while you’re writing CSS.

Personally, I think this isn’t a new idea and the reason we don’t see it in CSS already is because (my guess) there’s no way to make it perform as fast/well as what the rest of CSS does. I bet if they could figure out how to make it perform better, it would be something we would see.

CSS selectors (and document.querySelector()) let you select elements by matching the names and values of their attributes. It’s possible to match part of an attribute’s value using [attribute*=], [attribute^=], [attribute$=], but there’s no way to select an element based on matching only part of an attribute name.

Thankfully this is something we can do via XPath in all modern browsers (excluding IE11) like this:

document.evaluate(
  '//*[@*[starts-with(name(), "data-custom-")]]',
  document, 
  null, 
  XPathResult.UNORDERED_NODE_ITERATOR_TYPE, 
  null
)

This XPath selector, //*[@*[starts-with(name(), "data-custom-")]], returns all nodes with an attribute name that starts with data-custom-, so it would match elements with data-custom-name and data-custom-example, but not data-customizer.

For those wishing to select elements with this ability while authoring CSS, here are a few workarounds that can help:

@element * {
  eval("/^.*data-item-/.test(outerHTML) && '$this'") {
    background: lime;
  }
}
[test="/^.*data-item-/.test(this.outerHTML)"] {
  background: lime;
}
[xpath="//*[@*[starts-with(name(), 'data-item-')]]"] {
  background: lime;
}

Another thing CSS can’t do is select a tag by partial tag name match, and again, thankfully this is something we can do via XPath in modern browsers (>IE11) like this:

document.evaluate(
  '//*[starts-with(name(), "custom-")]',
  document, 
  null, 
  XPathResult.UNORDERED_NODE_ITERATOR_TYPE, 
  null
)

This XPath selector, //*[starts-with(name(), "custom-")], returns all nodes with a tag name that starts with custom-, so it would match <custom-tag> and <custom-example>, but not a tag named <customizer-panel>.

For those wishing to select elements in a similar way while authoring CSS, here are a few workarounds that can help:

@element * {
  eval("/^<custom-/.test(outerHTML) && '$this'") {
    border: 1px solid lime;
  }
}
  • [Partial tag name match with Selectory]
[test="/^custom-/.test(this.outerHTML)"] {
  border: 1px solid lime;
}
[xpath="//*[starts-with(name(), 'custom-')]"] {
  border: 1px solid lime;
}
[test="/^<h[\d]/.test(this.outerHTML)"] {
  color: lime;
}
[xpath="//div/*[starts-with(name(),'h')][substring(name(),2) > 0]"] {
  background: red;
}

Hopefully that helps give you some extra power as you’re looking to select elements in a more flexible way :smiley:


#4

Indeed, that is the same exact feature request.

In light of WebComponents, and people namespacing them due to the dash-in-name requirement, selecting something like

project-* {
  color: red;
}

might make sense with some collections of elements. But it can also just be useful. For example, suppose we just want to count how many A-Frame elements are in the page:

console.log(document.querySelector('a-*').length )

where project-* and a-* are just example of the possible syntax, but syntax similar to the email thread you linked could work.

That’s interesting about XPath, I never really knew about it. Looks useful! I guess it’d be easy to implement a CSS selector in the browser engine based off of it.


On a sidenote, the CSS selector polyfill stuff is interesting. It parses the CSS, then it basically turns into a CSS-in-JS solution inside of the implementation, if you will.

I’ve been making custom elements, and I’m wondering how I might be able to add custom CSS properties for them. Do you know of any frameworks that make this easy? I’m imagining a framework that abstracts away the parsing, etc, and lets the end user specify new properties.

I am aware of CSS Custom Properties, but from what I can tell, these are just shortcuts to map values from the custom properties to values of real CSS properties.

In my case, I’d like to map custom properties to properties of WebGL objects.

@MT @tomhodgins Do you guys know of a generic library designed to let one easily experiment with CSS polyfills (or basically implement new CSS properties that can apply to any element generically)?

For example, here’s a scene written with my custom elements:

I’d like to be able to “polyfill” certain CSS properties that so I can configure light intensity, mesh color, etc. Even mapping CSS transform to WebGL would be interesting. Making CSS animations work with it, etc.


#5

Hi @trusktr

So funny you replied to this when you did - I just featured a XPath selector mixin today on twitter as part of #merryCSSmas! https://twitter.com/innovati/status/941307794120232962

@MT @tomhodgins Do you guys know of a generic library designed to let one easily experiment with CSS polyfills (or basically implement new CSS properties that can apply to any element generically)?

Yes! I’ve been writing about this lately as well. It’s not so much a library as it is a pattern, or a way of doing things. I’ve found the easiest way to experiment with extending CSS using JavaScript to experiment with custom at-rules, custom selectors, custom properties, and custom property values is using a file like this:

  • a CSS stylesheet
  • that includes JS anywhere via ${}
  • read as a JS template string
  • output as CSS

As I’ve been developing plugins, ever since I created reproCSS I’ve been making all of my plugins read the same JS-in-CSS stylesheet format, and because of that all of the ‘mixins’ I make, the little plugins that you can use from your JIC stylesheets, all of the mixins will work with any JIC-processing plugin :smiley:

Here’s a little thing I wrote about extending CSS with JS-in-CSS stylesheets: https://gist.github.com/tomhodgins/5ae2d520152009f78558b6e32951f77e

In it, I show how you can polyfill something like:

@time (day: 'monday') {
  html {
    background: lime;
  }
}

Instead with a tiny JS function and something like this in your stylesheet:

${time (day: 'monday', `
  html {
    background: lime;
  }
`)}

So far I’ve created around a dozen mixins, here’s all of them in one file: http://staticresource.com/mixins.js

And if you’re looking for a playground where you can test it immediately, check out: http://staticresource.com/jicrepl.html which is an HTML scratchpad with HTML, JS-in-CSS, and JS, and a full-page live preview below. It also has the mixins.js file loaded in so you can test out the ${xpath()} here easily :smiley:

If you want another example of how JS-in-CSS makes it easy to extend CSS consider what I’m doing today, I have an audio file and I want to target different CSS rules depending on the current playback time of the audio file. I wish CSS had something like this:

@audio [src$=example.m4a] (60 < currentTime < 120) {
  .slide2 { opacity: 1 }
}

But there is no @audio at-rule in CSS, there are no at-rules that accept a selector to watch one element, and there definitely isn’t a currentTime media feature.

So instead, we can use JS-in-CSS and extend this with a teeny tiny JS function. We’ll need something like audio(selector, test, stylesheet) so we can give it a CSS selector for 1 element to watch (our audio file), we can supply a JavaScript function as a test, and then if the test is true, the stylesheet gets output to the page, otherwise it doesn’t. Here’s the function I ended up with:

function audio(selector, test, stylesheet) {

  var tag = document.querySelector(selector)

  return test(tag) ? stylesheet : ''

}

And the live demo of that: https://codepen.io/tomhodgins/pen/ppvxBb

So hopefully that’s a good example of extending CSS via JS-in-CSS in a way that doesn’t depend on your CSS being parsed at any point (you evaluate JS, but CSS only gets evaluated by the browser once you’re finished with it) and also doesn’t depend on any particular tech stack or that you’re using JS to assemble your HTML or anything (like many CSS-in-JS libs require).


#6

That’s cool stuff!!

So how would I write

@audio [src$=example.m4a] (60 < currentTime < 120) {
  .slide2 { opacity: 1 }
}

instead of JIC? That would require parsing obviously. It’d be cool to have some sort of parser API that makes it really easy to add these features without worrying about how the parsing is done.

f.e.

CSS.defineMixin('audio', function(selector, test, style) {
  // ...
})

then that wires it all up so that the page can simply have

<style>
  @audio [src$=example.m4a] (60 < currentTime < 120) {
    .slide2 { opacity: 1 }
  }
<style>

and also something like

  // lightyear unit, to blow out the memory:
  CSS.defineUnit('ly', function(value, el) {
    return /* a pixel value here I suppose */
  })

then just

<style>
  div {
    height: 1ly; /* REALLY tall div */
  }
</style>

#7

Thanks @trusktr! If you were looking to write a custom syntax like:

@audio [src$=example.m4a] (60 < currentTime < 120) {
  .slide2 { opacity: 1 }
}

I think the easiest way to add support for this might be using PostCSS. PostCSS would allow you to write a plugin that parsed and read this custom syntax, and then allow you to transform it using JavaScript.

For most of the things PostCSS does, it is outputting CSS, so most of the plugins that work for it are CSS preprocessors. Unfortunately for us, our @audio example can’t be expressed as CSS alone, so whatever PostCSS plugin you wrote to transform it would likely have to result in outputting both CSS and JavaScript that would make it work.

I believe this is possible, but it’s a big undertaking just to be able to use non-standard syntax by adding additional steps to your build process, when at the end you still require the JS runtime :thinking:


As for the custom CSS unit example - I have an idea! I’ve written a function that defined element-based units: EW, EH, EMIN, and EMAX. I want these to be ‘element-percentage’ units relative to an element’s width and height in a similar way to how ‘viewport-percentage’ units measure the viewport’s width and height.

The JS code for my units is this:

// Element-based units
function eunit(selector, rule) {

  let styles = ''
  let count = 0

  document.querySelectorAll(selector).forEach(tag => {

    const attr = selector.replace(/\W/g, '')

    rule = rule.replace(/(\d*\.?\d+)(?:\s*)(ew|eh|emin|emax)/gi,
      (match, number, unit) => {

        switch(unit) {

          case 'ew':
            return tag.offsetWidth / 100 * number + 'px'

          case 'eh':
            return tag.offsetHeight / 100 * number + 'px'

          case 'emin':
            return Math.min(tag.offsetWidth, tag.offsetHeight) / 100 * number + 'px'

          case 'emax':
            return Math.max(tag.offsetWidth, tag.offsetHeight) / 100 * number + 'px'

        }

      })

    tag.setAttribute(`data-eunit-${attr}`, count)
    styles += `[data-eunit-${attr}="${count}"] { ${rule} }\n`
    count++

  })

  return styles

}

And you could use it in a demo with JS-in-CSS like this:

<h2>Element Based Units</h2>

<div class=ew>EW Units</div>
<div class=eh>EH Units</div>
<div class=emin>EMIN Units</div>
<div class=emax>EMAX Units</div>

<style>
  div {
    margin: 15px;
    width: 100px;
    height: 100px;
    background: #ccc;
    border: 1px solid;
    overflow: auto;
    resize: both;
    line-height: 1;
  }
</style>

<script src=http://staticresource.com/helpers/eunit.js></script>

<script>
  // Reprocess on load, resize, input, and click
  window.addEventListener('load', JSinCSS)
  window.addEventListener('resize', JSinCSS)
  window.addEventListener('input', JSinCSS)
  window.addEventListener('click', JSinCSS)

  // Reprocess on mouse drag
  var click = false
  window.addEventListener('mousedown', e => click = true)
  window.addEventListener('mousemove', e => click && JSinCSS())
  window.addEventListener('mouseup', e => click = false)


  function JSinCSS() {

    var tag = document.querySelector('#JSinCSS')

    if (!tag) {

      tag = document.createElement('style')
      tag.id = 'JSinCSS'
      document.head.appendChild(tag)

    }

    tag.innerHTML = `

      ${eunit('.ew', `
        font-size: 15ew;
      `)}

      ${eunit('.eh', `
        font-size: 15eh;
      `)}

      ${eunit('.emin', `
        font-size: 15emin;
      `)}

      ${eunit('.emax', `
        font-size: 15emax;
      `)}

    `

  }
</script>

So you should be able to define new units in a similar way :smiley:


#8

This is all great for proof of concepts, but setting innerHTML on every change seems a bit expensive. Will Houdini supply ways to update thing programmatically (if ever)?

There’s also scenarios like "I’ve got Custom Elements that when composed render a 3D scene. How do I make visibility: hidden; hide one of those objects? The need there is not to add a new feature to CSS, but to make an existing feature do what it should to a new sort of element that renders with WebGL rather than regular DOM rendering.

For example,

<i-scene>
  <i-sphere size="30" has="phong-material" color="#ff6600" position="..."></i-sphere>
  <i-point-light color="white" position="..."></i-point-light>
</i-scene>

How can I polyfill new behavior for visibility: hidden or display: none so that the Sphere will be invisible or not rendered if I write

i-sphere {
  visibility: hidden;
  /*or*/
  display: none;
}

I wish there was some sort of high level API to easily polyfill functionality. f.e.

// this is the class for the `i-sphere` element
class Sphere extends HTMLElement { ... }

CSS.definePropertyForElement('display', Sphere, {
  none: function() { /* define how Sphere elements behave with display: none */ },
  // etc
})

CSS.definePropertyForElement('visibility', Sphere, {
  hidden: function() { /* define how Sphere elements behave with visibility: hidden */ },
  // etc
})

and this would take care of parsing, using MutationObserver to detect changes to styles in stylesheets as well as element style attributes, and whatever else may be necessary. An easy API. That’s just a rough suggestion, maybe the API would be different. But the main point is that the API would make it super easy to polyfill stuff.


#9

If you’re going to do this far you might as well use XPath selectors and not have to invent a custom syntax.