Skip to content

Instantly share code, notes, and snippets.

@Boldewyn
Last active January 21, 2021 19:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Boldewyn/ae5d54945f9044b6b42f to your computer and use it in GitHub Desktop.
Save Boldewyn/ae5d54945f9044b6b42f to your computer and use it in GitHub Desktop.
CSS Selectors to Match `label` Elements

CSS Selectors to Match label Elements

<label> elements are notoriously hard to select with CSS in any non-trivial mark-up structure, when the state of their control is to be taken into account. Consider these frequent cases, where it is not possible to reach the label element with CSS selectors, when the corresponding input state is relevant:

1. <label><input> Input inside label</label>

2. <tr>
    <td><input></td>
    <td><label>Good ol' tables</td>
  </tr>

3. <label>Label before input</label><input>

If you want to make the label react to the state of its associated control, you need to rely on JavaScript to generate classes to "listen" to. Only (1) would be addressed by a parent selector, e.g., $label input. Reference combinators only work the other way round, to address the control from its label: label /for/ input.

A clunky way to address a label from its input is to use :has():

:root:has(input#foo:invalid) label[for="foo"]

But it relies on the knowledge of the ID of the input element, if it has any. Case (1) above is then ruled out.

In many cases, the control element is partially or fully hidden, and the label acts as proxy to interact with the control, loved by designers for its independence from OS styles and ability to use ::before and ::after. In certain constellations handling this situation is already possible today with CSS only. Matt Mastracci played a bit with the thought of using CSS Extensions to expand on this. However, robust solutions would most probably still rely on JavaScript.

A pseudo-class :for() can provide better control over addressing label elements. Its content must be a single selector or selector list (mirroring :not()), that is matched against label.control. The specificity of :for() is the specificity of its arguments. Examples:

label:for([type="text"])             /* label.control.type === "text" */
label:for(:invalid)                  /* label.control.invalid === true */
label:for(:focus)                    /* label.control == document.activeElement */
label:for(:disabled):for(read-only)  /* chaining :for() works as expected */

If a label has no corresponding control, it will never match a :for() selector.

@pspeter3
Copy link

Did this ever go anywhere?

@CarlosMadeira
Copy link

This would definitely make my life easier.

@Boldewyn
Copy link
Author

Yes, for me, too. Still. Should take it back to the W3C mailing list again...

@Boldewyn
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment