abel-to-Input States

Sometimes there are very unusual mechanics hidden deep in the interactions of HTML and CSS. In this article I show one of those — the nuances of how HTML labels interact with inputs and CSS applied to them.

When fiddling with inputs & labels for them, I remembered one thing that amused me for a long timeGo to a sidenote. Look at this HTML:

<label>
  Here is an input:
  <input type="text" />
</label>

The fact that amuses me is that when you declare a state for an input in CSS, like with :hover or :active pseudo classes, and then you have a label for that input, then triggering those states over the label would actually trigger the same states on the input.

Here is an excerpt from the latest Selectors Level 4 spec:

Document languages may define additional ways in which an element can match :hover. For example, [HTML5] defines a labeled control element as matching :hover when its label is hovered.

Here is a simple example of this behaviour:

Hover or the input itself

If you’re using any modern browser, you could see the input to be highlighted both when you hover the input itself and the label associated with it.

The CSS for this example is trivial:

input.example:hover {
  background: lime;
}

ut What About Ancestors?

If you’d look at that place about the hover in the spec, you’d see the following sentence above:

An element also matches :hover if one of its descendants in the flat tree (including non-element nodes, such as text nodes) matches the above conditions.

Which is a thing you could possibly already know: when you hover over a descendant, then this state is also triggered on all the ancestorsGo to a sidenote, which can be used in a lot of useful ways.

But what if we have those connected label and input, and the label would be placed not in one of the input’s ancestor’s flat tree?

Like this:

<ul>
  <li>
    Here is
    <label class="example-label" for="Example2">a label</label>
    for the input inside another list item. Hover it!
  </li>
  <li>
    And here is the input for the above label:
    <input class="example" id="Example2" type="text" />
  </li>
</ul>

And if we’d add the same CSS as for the example above, we’d get the same result:

  • Here is for the input inside another list item. Hover it!
  • And here is the input for the above label:

For now the result is the same. But there is one big difference. Let’s add some styles like this:

ul.example3 > li:hover {
  box-shadow: 0 0 0 3px blue;
}

Now look at the following example and try to hover over the input itself and the associated label:

  • Here is for the input inside another list item. Hover it!
  • And here is the input for the above label:

You should see there that, as probably expected, only the actual list item that is hovered would obtain the above styles. Giving the input the :hover state using the label trick won’t trigger the :hover over its parent. All like was written in the specs.

And what this also means is that as we have this behaviour, the following selector that could look like nonsense at the first glance would actually mean something:

:not(:hover) > input#Example4:hover {
  background: blue;
}

If we’d take our first example and would just wrap an input into a simple span:

<span><input class="example" id="Example4" type="text" /></span>

This new selector would actually target onlyGo to a sidenote the hover state of this input that is caused by hovering over label.

And that means that we can now actually style both hover states in different ways!

Here is that example working:

Hover or the input itself

electing Siblings

As with a lot of other places in CSS, you could research a lot of things surrounding this behaviour. For example, not only the :hover state can be delegated this way, but also at least the :active one.

But I’d like to mention one nuance that could be rather useful, but which is prevented by IE/Edge not supporting it.

As we saw, the parent is, per spec, won’t get the hover state of such delegated :hover. But what if we’d want to use this :hover as a prerequisite to something else? What if we would add a combinator after it and then would try to select some siblings that will come after the input?

Like this:

  • Here is for the input inside another list item. Hover it!
  • And here is the input for the above label: , now with a link after it.

In Firefox and browsers based on Webkit/Blink you could see the link that goes after the input to be highlighted even when you hover the label inside another list item! In Edge, sadly, nothing happens there.

And, as in the browsers that support it, this behaviour would work even if the input is disabled, we then could hide it using the often used technique (using clip etc.), and then use labels at one part of the page to highlight stuff at any other part of the page without any JS. What fun could we get out of it?

reaking the Specs in the End

The most fun thing what I found when doing quick experiments is this:

  • Here is some list item with some words like Woosh and Wheee
  • And another list item!
  • And another!

Hey, look, there is a Hover it!

Sadly, there I used the forbidden in specs nesting of labels inside other labels (which we could actually overcome in terms of validity using either the method for nesting links or by DOM manipulation), which surprisingly works as we’d expect: you hover one (visually) item, and then you get visual feedback from any number of places all over the page.

Imagine if this would be the thing we could use without hacks and without relying on the label-to-input state delegation.

And here we have it implemented in some way, so its entirely possible to do in browsers and we’d only need the specs to support it? What do you think, do we need a way to delegate states from one element to another in native CSS? If you think that we need, tell your fellow spec writers and/or browser vendors, or even come up with a draft of what it could look like in a spec by yourself!

There is a chance, that such stuff would be possible with extensive usage of :has() (at the same spec) if it would be ever implemented, but yeah, only if it would be ever implemented.


Let me know what you think about this article on Mastodon!