Invalid CSS Selector Causes Rule to Be Dropped: What Is the Rationale

Invalid CSS selector causes rule to be dropped: What is the rationale?

Why is this desirable? Why doesn't the spec suggest simply discarding the unrecognised selector, but keeping the rest of the rule?

The short answer is because it'd be too difficult for implementations to figure out what exactly constitutes "the rest of the rule" (or "the rest of the selector list" for that matter) without getting it wrong and inadvertently messing up layouts, as well as for consistency in error handling, and forward compatibility with future specifications.


I'll preface my long answer with a link to another answer of mine, on handling of invalid selectors. A comment on that answer points directly to section 4.1.7 of the CSS2.1 spec on dealing with errors in selectors within rule sets, which mentions commas in selectors as an example. I think it sums it up pretty nicely:

CSS 2.1 gives a special meaning to the comma (,) in selectors. However, since it is not known if the comma may acquire other meanings in future updates of CSS, the whole statement should be ignored if there is an error anywhere in the selector, even though the rest of the selector may look reasonable in CSS 2.1.

While the comma itself still means grouping two or more selectors as far as selectors are concerned, it turns out that Selectors 4 introduces new functional pseudo-classes that accept selector groups (or selector lists) as arguments, such as :matches() (it even changes :not() so it accepts a list, making it similar to :matches(), whereas in level 3 it only accepts a single simple selector).

This means that not only will you find comma-separated groups of selectors associated with rules, but you'll start finding them within functional pseudo-classes as well (note that this is within a stylesheet only; outside of CSS, selectors can appear in JavaScript code, used by selector libraries and the native Selectors API).

Although not the only reason by far, this alone is enough to potentially over-complicate a parser's error handling rules with a huge risk of breaking the selector, the rule set, or even the layout. In the event of a parsing error with a comma, the parser will have trouble determining whether this selector group corresponds to an entire rule set, or part of another selector group, and how to handle the rest of the selector and its associated rule set accordingly. Instead of trying to guess, risk guessing wrongly and breaking the rule in some way (e.g. by matching and styling all the wrong elements), the safest bet is to discard the rule and move on.

As an example, consider the following rule, whose selector is valid in level 4 but not in level 3, taken from this question of mine:

#sectors > div:not(.alpha, .beta, .gamma) {
color: #808080;
background-color: #e9e9e9;
opacity: 0.5;
}

A naïve parser that doesn't understand Selectors 4 may try to split this into three distinct selectors that share the same declaration block, instead of a single selector with a pseudo-class that accepts a list, based on the commas alone:

#sectors > div:not(.alpha
.beta
.gamma)

If it simply discards the first and last selectors which are obviously invalid, leaving the second selector which is valid, should it then try to apply the rule to any elements with class beta? It's clearly not what the author intends to do, so if a browser does that, it's going to do something unexpected to this layout. By discarding the rule with the invalid selector, the layout looks just a little saner, but that's an over-simplified example; rules with layout-altering styles can cause even bigger problems if applied wrongly.

Of course, other ambiguities in selector parsing can occur too, which can lead to the following situations:

  • Not knowing where the complex selector ends
  • Not knowing where the selector list ends
  • Not knowing where the declaration block begins
  • A combination of the above

All of which, again, are most easily resolved by discarding the rule set instead of playing the guessing game.

In the case of seemingly well-formed selectors that are unrecognized, such as :last-child as a pseudo-class in your example, the spec makes no distinction between unrecognized selectors and selectors that are just plain malformed. Both result in a parsing error. From the same section that you link to:

Invalidity is caused by a parsing error, e.g. an unrecognized token or a token which is not allowed at the current parsing point.

And by making that statement about :last-child I'm assuming the browser is able to parse a single colon followed by an arbitrary ident as a pseudo-class in the first place; in reality you can't assume that an implementation will know to parse :last-child as a pseudo-class correctly, or something like :lang() or :not() with a functional notation since functional pseudo-classes didn't appear until CSS2.

Selectors defines a specific set of known pseudo-classes and pseudo-elements, the names of which are most likely hardcoded in every implementation. The most naïve of parsers have the entire notation for each pseudo-class and pseudo-element, including the single/double colon(s), hardcoded (I wouldn't be surprised if the major browsers actually do this with :before, :after, :first-letter and :first-line as a special case). So what may seem like a pseudo-class to one implementation might very well be gobbledygook to another.

Since there are so many ways for implementations to fail, the spec makes no distinction, making error handling much more predictable. If a selector is unrecognized, no matter whether it's because it's unsupported or malformed, the rule is discarded. Simple, straightforward, and easy enough to get your head around.


All that said, there is at least one discussion in the www-style public mailing list suggesting that the specification be changed because it may not be so difficult to implement error handling by splitting selectors after all.

I should also mention that some layout engines behave differently, such as WebKit ignoring non-WebKit-prefixed selectors in a rule, applying its own prefixes, while other browsers ignore the rule completely (you can find more examples on Stack Overflow; here's a slightly different one). In a way you could say WebKit is skirting the rule as it is, although it does try to parse comma-separated selector groups smartly in spite of those prefixed selectors.

I don't think the working group has a compelling reason to change this behavior yet. In fact, if anything, they have a compelling reason not to change it, and that's because sites have been relying on this behavior for many years. In the past, we had selector hacks for filtering older versions of IE; today, we have prefixed selectors for filtering other browsers. These hacks all rely on the same behavior of certain browsers discarding rules they don't recognize, with other browsers applying them if they think they're correct, e.g. by recognizing prefixes (or throwing only the unrecognized ones out, as WebKit does). Sites could break in newer versions of those browsers if this rule were to change, which absolutely cannot happen in such a diversified (read: fragmented) Web as ours.

As of April 2013, it was decided in a telecon that this behavior remain unchanged for the reason I've postulated above:


- RESOLVED: Do not adopt MQ-style invalidation for Selectors
due to Web-compat concerns.

Media query-style invalidation refers to invalid media queries in a comma-separated list not breaking the entire @media rule.

What happens when the browser doesn't support a CSS pseudo-class?

Browsers currently make no distinction between unrecognized or unsupported selectors, and invalid selectors. If a browser recognizes a selector, generally it'll have implemented it to the best of its ability (and any behavior that's not to spec can be classified as a bug on its bug tracker), even if it doesn't implement all other features in the same level of Selectors (as is currently the case with :dir() for example, and historically Internet Explorer 7 and 8 with level 3 attribute selectors, and Internet Explorer 6 with the universal selector). If it doesn't recognize the selector, it follows CSS2.1 §4.1.7 to the letter and drops the entire ruleset, no questions asked. Notice that it says

When a user agent cannot parse the selector (i.e., it is not valid CSS 2.1), it must ignore the selector and the following declaration block (if any) as well.

which implies that if a user agent cannot parse a selector, it must therefore be invalid CSS2.1 (or invalid in some other level of Selectors); and inversely that if it can parse a selector, it must therefore be valid. But this assumes the user agent conforms fully to the standard; we all know that in reality, different implementations have different levels of conformance to each standard, and certain implementations even have their own vendor-specific selectors which are not part of any standard. So I treat this as "When a user agent cannot parse the selector" without the parenthetical, and I imagine browser vendors do the same.

In fact, Selectors itself makes no distinction between a pseudo-class token with an ident or function that doesn't correspond to a valid pseudo-class, and a series of characters that cannot even be parsed as a pseudo-class — they're both equally invalid — see section 12 of css3-selectors and section 3.9 of selectors-4. Essentially, this means that the current browser behavior is in full compliance with the standard, and not just an arbitrary decision agreed upon by browser vendors.

I've not heard of any browser that recognizes a pseudo-class as valid for the purposes of error handling, and proceeds to ignore either just that pseudo-class or the entire complex selector (leaving other complex selectors in the selector-list unaffected). WebKit did use to have a really bad habit of accepting CSS rules with unrecognized pseudo-elements, allowing things like ::selection, ::-moz-selection, which proved useless anyway because every other layout engine followed the spec more strictly. I believe WebKit no longer does this, however, but you know how WebKit is with these things. But AFAIK it has never done this with pseudo-classes.

On the standards side, selectors-4 appears set to change this with the introduction of the static and dynamic profiles. My email on the subject was addressed in a CSSWG telecon; you can find the minutes here (search for "Behavior of Selectors not in Fast Profile"). However, it was resolved that selectors not in the dynamic (previously fast) profile should be treated as invalid and cause the entire CSS rule to be dropped, as usual. See section 2.1:

CSS implementations conformant to Selectors Level 4 must use the dynamic profile for CSS selection. Implementations using the dynamic profile must treat selectors that are not included in the profile as unknown and invalid.

Why do CSS comma separated selectors break entire rule when one part is unknown?

It turns out this is actually intentional and defined in Selectors Level 3 (emphasis by me):

If just one of these selectors were
invalid, the entire group of selectors would be invalid. This would
invalidate the rule for all three heading elements, whereas in the
former case only one of the three individual heading rules would be
invalidated.

Invalid CSS example:

h1 { font-family: sans-serif }
h2..foo { font-family: sans-serif }
h3 { font-family: sans-serif }

is not equivalent to:

h1, h2..foo, h3 { font-family: sans-serif }

because the above selector (h1, h2..foo, h3) is entirely invalid and
the entire style rule is dropped. (When the selectors are not grouped,
only the rule for h2..foo is dropped.)


CSS 2 didn't specify what to do when one selector was wrong. In fact the W3C spec states that the condensed form is equivalent to the written out version:

In this example, we condense three rules with identical declarations
into one. Thus,

h1 { font-family: sans-serif }
h2 { font-family: sans-serif }
h3 { font-family: sans-serif }

is equivalent to:

h1, h2, h3 { font-family: sans-serif }

EDIT: (thx @BoltClock):

CSS 2.1 does specify the behavior and it is the same as with CSS3:

(...) since the "&" is not a valid token in a CSS 2.1 selector, a CSS 2.1 user agent must ignore the whole second line.

How does the _:-ms-lang(x) fix for Edge and IE work?

In CSS, when a browser does not recognize part of a selector (or thinks there is an error in a selector), it completely ignores the entire rule.

Here's the section in the CSS3 spec outlining this behavior

The prelude of the qualified rule is parsed as a selector list. If this results in an invalid selector list, the entire style rule is invalid.

Here CSS2.1 talks about the special case of comma

CSS 2.1 gives a special meaning to the comma (,) in selectors. However, since it is not known if the comma may acquire other meanings in future updates of CSS, the whole statement should be ignored if there is an error anywhere in the selector, even though the rest of the selector may look reasonable in CSS 2.1.

Therefore when the other browsers try to parse the selectors, they find the _:-ms-lang(x) selector to be invalid and so ignore the entire rule (including .selector)

Also here is an excellent answer on why this behavior is desirable

Why can't I combine pseudo-element selectors?

You are combining multiple vendor-specific selectors into a single CSS rule.

This means that if one of the selectors is not recognised by the browser, the entire CSS block is ignored. In this particular case, Chrome does not recognize ::-moz-range-track, because it is specific to Firefox/Gecko. This is not a quirk, but intended behaviour and part of the CSS standard.

The solution would be to split the declarations. Like so:

.range2::-webkit-slider-runnable-track {
height: 6px;
border-radius: 3px;
border: 1px solid black;
}
.range2::-moz-range-track {
height: 6px;
border-radius: 3px;
border: 1px solid black;
}

Updated CodePen

Firefox ::-moz-selection selector bug(?) is there a workaround?

Firefox appears to simply not understand ::selection (hence necessitating the vendor-prefixed ::-moz-selection), so it ignores the entire rule on encountering an unrecognized selector per the spec.

The common workaround for a browser not understanding one or more selectors in a group is to split/duplicate the rule set:

/* Firefox sees this */
.green ::-moz-selection {
background-color: #62BA21;
color: white;
}

/* Other browsers see this */
.green ::selection {
background-color: #62BA21;
color: white;
}

In fact, in this case it's the only thing you can do, i.e. you will have to put up with this slight bit of bloat.

jsFiddle demo

IE8 breaks when using two selectors?

I would try making the style separately (without the comma). IE8 is probably not recognizing the :nth child and skipping the declaration.

Is there an explanation for why we cannot place semicolons between CSS declaration blocks?

In CSS, rules are defined by either blocks, or statements, but not both at the same time. A block is a chunk of code that is surrounded by a pair of curly braces. A statement is a chunk of code that ends with a semicolon.

An empty "rule" is not a valid CSS rule, because it cannot be parsed as either a qualified rule or an at-rule. So it stands to reason that a lone ; between two blocks is invalid, for the same reason that a block that doesn't contain a prelude (either a selector-list, or an at-keyword followed by an optional prelude) is invalid: because it cannot be parsed into anything meaningful.

Only at-rules may take the form of statements and therefore be terminated by a semicolon (examples include @charset and @import); qualified rules never do. So when a malformed rule is encountered, if the parser isn't already parsing an at-rule, then it is treated as a qualified rule and everything up to and including the next matching set of curly braces is consumed and discarded, including the semicolon. This is described succinctly in section 2.2 of css-syntax-3 (it says the text is non-normative, but that's only because the normative rules are defined in the grammar itself).

And the reason error handling takes such an eager approach in CSS is mostly due to selector error handling — if it were conservative, browsers might end up inadvertently parsing the following rule as something completely unexpected. For example, if IE6, which doesn't understand >, were to ignore just the p > in p > span {...} and regard everything starting with span as valid, the rule would end up matching any span element in IE6, whilst matching only the appropriate subset of elements in supporting browsers. (In fact, a similar issue does exist in IE6 with chained class selectors — .foo.bar is treated as .bar.) You could think of this, therefore, not as liberal error handling, but conservative application of CSS rules. Better not to apply a rule when in doubt than apply it with unexpected results.

Whoever told you it was for performance reasons is just making it up.



Related Topics



Leave a reply



Submit