CSS Validation: Common Mistakes and How to Fix Them
Learn what CSS validation checks, how browsers drop invalid CSS, and how to fix the most common mistakes, from missing semicolons to calc() spacing.

You change one line of CSS, reload the page, and nothing happens. Or worse, half of a component looks right and the other half silently ignores your rules. There is no error in the console, no red text, no stack trace. The stylesheet simply did less than you asked. This is how CSS fails, and it is the reason a validator is such a useful tool: it tells you out loud what the browser decided to ignore quietly.
This guide is for front-end developers, designers who write their own styles, and anyone who maintains a stylesheet that has grown over the years. You will learn what CSS validation checks and what it cannot check, how browsers recover from invalid CSS (with results measured in a real browser, shown below), a catalogue of the most common mistakes with before and after fixes, how validators lag behind modern features, how linting differs from validation, and how to put both into your build pipeline.
What is CSS validation?
CSS validation means checking a stylesheet against the grammar defined in the CSS specifications. A validator reads your text and asks two kinds of question. First, is the syntax well formed: are the braces balanced, are declarations separated by semicolons, are strings and comments closed? Second, does each property exist, and is each value legal for that property: is margin: 10px allowed, and is margin: 10 allowed?
The answers come from the specifications, not from any browser. That is the main difference between validation and testing. A validator does not render anything. It compares your text with a formal description of what the language permits.
What a validator does not check
Many people expect more from a green "no errors" result than it can promise. A valid stylesheet can still be broken in practice. Validation does not tell you:
- Whether a browser supports a feature. A declaration can be perfectly valid CSS and still be ignored by an older browser. Support data lives in compatibility tables such as the ones on MDN, not in a validator.
- Whether the page looks right. A rule that sets
color: whiteon a white background is valid and unreadable. - Whether a selector matches anything.
.buton { ... }is valid even if your markup saysclass="button". The validator cannot know your HTML. - Performance. A stylesheet with expensive selectors or three megabytes of unused rules passes validation.
- Accessibility. Colour contrast, focus visibility and motion preferences are outside the grammar.
- Whether your intent is correct.
margin: 0 10px 0 10pxis valid even if you wantedmargin: 0 0 10px.
Keep that list in mind. Validation answers one narrow question very reliably: "Is this text legal CSS?" It is a fast first check, and it is a poor substitute for looking at the result in a browser.
A short history of CSS and its validators
CSS (Cascading Style Sheets) was proposed in the mid-1990s to separate the presentation of a web page from its structure, and the W3C published CSS level 1 as a Recommendation in the 1990s. CSS level 2 followed, and then the W3C stopped publishing the language as one huge monolithic version. Since CSS3, the language has been split into modules, each with its own specification and its own maturity level: selectors, colour, values and units, flexbox, grid, and many more. That is why you will see people write "CSS3" even though no single document by that name defines everything.
Early on, browsers disagreed widely about how to interpret CSS, and authors wrote defensive code full of hacks. A validator gave them a neutral referee. The W3C CSS Validation Service, usually called "Jigsaw", has been used for many years to check pages and stylesheets against the specifications.
Another important historical choice is that CSS was designed to be forward compatible. The specification defines exactly how a parser must behave when it meets something it does not understand: skip the bad part and carry on. This rule is what allowed authors to use new features while keeping old browsers working. It is also what makes CSS errors silent. We will look at the recovery rules in detail in the next section, because they explain almost every confusing symptom you will meet.
How browsers handle invalid CSS
Most programming languages stop at the first syntax error. CSS does not. The specification (CSS Syntax Module) describes error handling as part of the grammar. A browser that finds something it cannot parse drops just enough input to get back to a safe point, and then continues. Three rules explain most of what you will observe.
- An invalid declaration is dropped. The property and value that failed are ignored, and the other declarations in the same rule still apply.
- An invalid selector drops the whole rule. If any selector in a comma-separated list is invalid, none of the selectors in that list apply, and the declarations are discarded with them.
- A broken at-rule is skipped as a unit. An unknown or malformed
@mediaor similar rule is skipped, including everything in its block.
There is a fourth, subtler effect: the parser decides where a declaration ends by looking for a semicolon or a closing brace. If you forget a semicolon, two declarations merge into one string of tokens, and that merged declaration is usually invalid.
A measured experiment in a real browser
Rules are easier to trust when you can see them work. The test page below defines several deliberately broken rules, adds elements with matching classes, and reads back the computed styles with getComputedStyle. I ran it in headless Google Chrome (the "Chrome for Testing" build, version 153) on the machine used to write this article. Results can differ in other browsers and versions, but these behaviours are defined by the specification, not by one vendor.
<style>
.t1 { color: red; margin: 10; padding: 4px; }
.t2, .t3:hovr { color: blue; }
.t5 { display: flex
width: 50%; color: green; }
.t6 { color: red !improtant; padding: 1px; }
.t7 { color: red; color: nonsense; }
.t8 { color: purple; border = 1px solid; padding: 2px; }
// comment
.t9 { color: teal; }
.t10 { color: navy; }
@media screen and (min-width 600px) { .t11 { color: olive; } }
.t12 { color: #12345; margin: 3px; }
</style>
Each element carried the class of the same name, and the script printed the computed colour, margin and padding. The results are summarised here.
| Rule | What was invalid | Result observed |
|---|---|---|
.t1 |
margin: 10 (no unit) |
margin dropped (0px); color red and padding 4px kept |
.t2, .t3:hovr |
misspelled pseudo-class in the second selector | whole rule dropped; .t2 stayed black even though its own selector was fine |
.t5 |
missing semicolon after display: flex |
display and width both dropped; color: green kept |
.t6 |
!improtant |
color dropped (black); padding: 1px kept |
.t7 |
color: nonsense after a valid color: red |
color stayed red, because the invalid duplicate was dropped |
.t8 |
border = 1px solid |
border dropped; color: purple and padding: 2px kept |
.t9 |
preceded by a // comment line |
.t9 rule dropped (black); the following .t10 rule worked (navy) |
.t11 |
(min-width 600px) without a colon |
whole @media block ignored |
.t12 |
color: #12345 (five hex digits) |
color dropped; margin: 3px kept |
Three of these deserve a closer look, because they surprise people.
The .t2 result. The selector list contains one valid and one invalid selector. Because the list is parsed as a unit, one typo disables both. This is the reason a vendor-prefixed pseudo-class, which some browsers do not know, must live in its own rule rather than sharing a list with a standard selector. We return to that under vendor prefixes.
The .t5 result. Without a semicolon, the parser reads display: flex width: 50% as one declaration whose value is flex width: 50%. That value is not legal for display, so the whole declaration is dropped. You lose two properties for one missing character, and the property after the broken pair (color: green) is rescued only because it ends with its own semicolon.
The .t9 result. A // line is not a comment in CSS. The parser reads // comment .t9 as the beginning of a selector, which is invalid, so the rule that follows is dropped. The rule after that is unaffected.
The two cases that swallow more than one rule
Two mistakes affect much more than one declaration. I tested both in the same browser.
An unclosed comment swallows everything after it. A /* with no */ turns the rest of the stylesheet into a comment, and an element styled only by a rule below it kept its default colour in my test.
A missing closing brace behaves differently in modern browsers than it did in older ones. Because CSS nesting is now part of the language, a rule that appears inside an unclosed block can be parsed as a nested rule. In my test, this stylesheet had no closing brace after the .u2 declaration:
.u1 { color: red; }
.u2 { color: blue;
.u3 { color: green; }
.u4 { color: orange; }
.u1 and .u2 worked. Elements with the classes .u3 and .u4 stayed black, because both rules were read as nested inside .u2, so their selectors became ".u3 inside .u2" and ".u4 inside .u2". The browser closed the open block at the end of the file. The practical lesson is that one missing brace can disable every rule after it, and the symptom is "everything below this point stopped working".
Duplicate and fallback declarations
The .t7 row shows something useful rather than harmful. If a browser drops an invalid declaration, an earlier valid one stays in effect. Authors have used this on purpose for years: write the safe value first, and a newer value after it. A browser that does not understand the second value ignores it, and uses the first. We will use this in the section on duplicate properties.
How a validator reads your stylesheet
Understanding the process helps you read validator messages. A validator works in two stages.
Stage 1: tokenising and parsing. The input is split into tokens: identifiers, numbers with units, strings, colons, semicolons, braces. The parser assembles them into rules and declarations according to the syntax rules. Errors found here are syntax errors such as "unexpected end of file" or "colon is expected".
Stage 2: matching values against the grammar. For each declaration, the validator looks up the property and compares the value with the formal syntax for that property. The specifications describe these grammars in a compact notation. You see it in the MDN "Formal syntax" boxes, for example <length> | <percentage> | auto. If the value does not match any alternative, the validator reports an invalid value.
I ran the open-source parser css-tree (version 3.2.1, installed from npm) on a small broken sample to show both stages. The sample was:
.card {
colour: red;
margin: 10;
padding: 1em 2em 3em 4em 5em;
background: #12345;
display: flex
width: 50%;
}
.btn {
font-family: Open Sans, sans-serif;
color: red !improtant;
border = 1px solid;
}
The script walked every declaration and asked css-tree's lexer whether the value matched the property's grammar. The output was:
parse errors:
7:8 Unexpected input
12:10 Colon is expected
value check:
line 2: colour:red -> Unknown property `colour`
line 3: margin:10 -> Mismatch
line 4: padding:1em 2em 3em 4em 5em -> Mismatch
line 5: background:#12345 -> Mismatch
line 6: display:flex
width: 50% -> Mismatch
Notice what each stage found. The syntax stage caught the stray = (a colon is expected) and noted the input after the missing semicolon. The value stage caught the unknown property, the unitless length, the five-value padding, the five-digit hex colour and the merged display declaration. It did not flag !improtant or the unquoted font name, and that is correct for this particular tool: its value check does not model every corner of the grammar. This is an honest reminder that different validators have different strengths, and none of them is perfect. The W3C validator and other tools may report such lines in a different way, so compare more than one when a message puzzles you.
The catalogue of common mistakes
The following sections show the most common errors in order of how often people trip over them. Every "after" example below was checked against the same parser or against the real browser used above.
1. Missing semicolon
The most common error, and the most damaging one, as the experiment showed.
/* Before */
.card {
display: flex
gap: 1rem;
color: #333;
}
/* After */
.card {
display: flex;
gap: 1rem;
color: #333;
}
Get in the habit of ending the last declaration of a block with a semicolon as well. It is legal either way, but if you add a line after it later, nothing breaks. Many formatters and linters can enforce this.
2. Missing or extra brace
/* Before: the first rule never closes */
.nav { display: flex;
.nav a { color: inherit; }
/* After */
.nav { display: flex; }
.nav a { color: inherit; }
An editor that highlights matching braces catches this. A validator reports it as an unexpected end of the file or a parse error close to the end, which can be far from the real mistake. When the reported line makes no sense, work backwards from there and look for the first rule that does not close.
3. Misspelled property names
/* Before */
.box {
colour: red;
backgroud-color: #eee;
margin-botom: 8px;
}
/* After */
.box {
color: red;
background-color: #eee;
margin-bottom: 8px;
}
British spelling causes a surprising share of these, since the language uses American spelling (color, center, gray). Validators report "unknown property" or "property doesn't exist". Custom properties are the exception. Anything starting with two hyphens is a custom property and is not checked against a list of known names.
4. Missing unit
A bare number is only valid for zero, and for a handful of properties that take unitless numbers, such as line-height, opacity, flex-grow and z-index.
/* Before */
.box {
margin: 10;
width: 300;
font-size: 16;
}
/* After */
.box {
margin: 10px;
width: 300px;
font-size: 16px;
}
The value 0 needs no unit (margin: 0), and writing 0px is also fine. Because margin: 10 was dropped in my experiment, the element simply received no margin, with no warning.
A related mistake is a space between the number and the unit. 10 px is two tokens, a number and an identifier, so it is not a length.
5. Wrong colour formats
/* Before */
.a { color: #12345; } /* five digits */
.b { color: #ggg; } /* not hexadecimal */
.c { color: rgb(255, 0); } /* two arguments */
.d { color: 123456; } /* missing # */
/* After */
.a { color: #123456; }
.b { color: #ccc; }
.c { color: rgb(255, 0, 0); }
.d { color: #123456; }
A hex colour must have 3, 4, 6 or 8 digits. The extra two forms include an alpha channel. Modern syntax also allows rgb(255 0 0 / 50%), with spaces and a slash for alpha. Mixing the old comma syntax and the new space syntax in the same function is an error, so rgb(255, 0, 0 / 50%) is invalid. In my browser test, CSS.supports("color", "#12345") returned false and CSS.supports("color", "rgb(255 0 0 / 50%)") returned true.
6. Invalid selectors and pseudo-classes
/* Before */
a:hovr { color: red; }
.menu > > li { margin: 0; }
li:nth-child(odd { color: gray; }
input::placehodler { color: #999; }
/* After */
a:hover { color: red; }
.menu > li { margin: 0; }
li:nth-child(odd) { color: gray; }
input::placeholder { color: #999; }
Remember rule 2: an invalid selector throws away the whole rule, and every selector in the same list with it. Misspelled pseudo-classes and pseudo-elements are the usual cause. Stylelint's selector-pseudo-class-no-unknown rule reported Unknown pseudo-class selector ":hovr" on my sample.
A single colon versus a double colon is a related trap. Pseudo-elements use two colons (::before, ::placeholder) and pseudo-classes use one (:hover). Browsers accept the single-colon form for a few legacy pseudo-elements such as :before, but write the double-colon form in new code.
7. Vendor prefixes and validator warnings
Vendor prefixes such as -webkit- and -moz- were a way for browsers to ship experimental features without claiming the final name. Many are now unnecessary, but real stylesheets still contain them, and some prefixed features (-webkit-line-clamp is a well-known case) remain in use.
Validators generally treat vendor-prefixed properties as outside the standard grammar. The W3C service usually reports them as warnings about vendor extensions rather than as errors, so you can pass validation with warnings. Read the warnings, but do not remove useful prefixed lines just to silence them.
The important browser behaviour is the one from the .t2 row. Put prefixed selectors in their own rules:
/* Before: if a browser does not know ::-moz-placeholder,
it drops the whole list, including the standard selector */
input::placeholder,
input::-moz-placeholder { color: #999; }
/* After: separate rules, so each browser keeps what it understands */
input::placeholder { color: #999; }
input::-moz-placeholder { color: #999; }
For declarations, an unknown prefixed property is just dropped. Place the prefixed declaration before the standard one, so the standard one wins in browsers that understand both. Automated tools such as Autoprefixer can add and remove prefixes based on your browser targets, which is more reliable than maintaining them by hand.
8. Unquoted font names
A font family that is a sequence of plain identifiers can be written without quotes. font-family: Open Sans, sans-serif; is legal, and my test with css-tree did not flag it. The problems arise with names that are not valid identifiers.
/* Before */
.a { font-family: Font 2, serif; } /* digit starts the second word */
.b { font-family: Source Sans 3, sans-serif; }
.c { font-family: Times New Roman, serif; } /* legal, but fragile */
/* After */
.a { font-family: "Font 2", serif; }
.b { font-family: "Source Sans 3", sans-serif; }
.c { font-family: "Times New Roman", serif; }
A bare number cannot be part of an unquoted family name, so the first two are invalid and the browser drops the declaration. In my parser run, Font 2, serif was reported as a mismatch. The third is legal, but quoting any family name that contains a space is the safer habit, and it prevents a name that looks like a keyword from being misread. Generic families such as serif, sans-serif and monospace must never be quoted, since a quoted name is looked up as a real font.
9. Invalid @media syntax
/* Before */
@media screen and (min-width 600px) { .a { color: red; } }
@media screen and min-width: 600px { .a { color: red; } }
@media (min-width: 600px) and (max-width: 400px) { .a { color: red; } }
@madia print { .a { color: red; } }
/* After */
@media screen and (min-width: 600px) { .a { color: red; } }
@media screen and (min-width: 600px) { .a { color: red; } }
@media (min-width: 400px) and (max-width: 600px) { .a { color: red; } }
@media print { .a { color: red; } }
Media features go in parentheses and use a colon, just like declarations. A condition that can never be true, as in the third "before" line, is syntactically valid and simply never applies, which is a logic bug a validator will not catch. A misspelled at-rule is skipped entirely. The Stylelint rule media-feature-name-no-unknown flagged min-widht in my test, and at-rule-no-unknown flagged @madia.
Modern range syntax such as @media (width >= 600px) is valid in current specifications. Older validators and tools may not accept it, which we discuss in the section on modern features.
10. !important typos and spacing
/* Before */
.a { color: red !improtant; }
.b { color: red ! important; }
.c { color: red !importnat; }
/* After */
.a { color: red !important; }
.b { color: red !important; }
.c { color: red !important; }
A misspelled flag makes the declaration invalid, so it is dropped. In my test, color: red !improtant left the element black. Whitespace between ! and important is allowed by the specification, but nobody reads it that way, so write it tight. The better question is whether you need !important at all. If you reach for it often, the underlying problem is usually specificity, and our guide to CSS formatting and architecture covers structuring selectors so that you do not have to.
11. // comments
CSS has exactly one comment syntax, /* ... */. A // line is not a comment, and it corrupts whatever follows.
/* Before */
// Main layout
.page { display: grid; }
/* After */
/* Main layout */
.page { display: grid; }
In my experiment, the rule after a // line was dropped. Sass and Less allow // comments in their own syntax and remove them during compilation, so the mistake often slips in when someone copies code from a preprocessor file into plain CSS.
12. = instead of :
/* Before */
.a { color = red; }
/* After */
.a { color: red; }
This is a habit carried over from other languages, for example from HTML attributes or JavaScript. The result in my test was a dropped declaration, with the neighbouring declarations unaffected.
13. Uppercase and lowercase
Most of the language is case-insensitive. Property names, keywords, units and hexadecimal digits can be written in either case, and my test with COLOR: Red; BACKGROUND-COLOR: #ABC produced the expected red text and a light grey-blue background. Case sensitivity appears in a few places:
- Class names and IDs in selectors are matched against the HTML. In standards mode,
.Buttonand.buttonare different classes. - Custom property names are case-sensitive:
--Gapand--gapare different properties. - Attribute values in selectors can be case-sensitive depending on the attribute and document language.
- URLs and font family names may be case-sensitive on the server or the operating system.
- Element names in HTML selectors are matched case-insensitively, but in XML-based documents such as SVG they are case-sensitive.
Pick lowercase for properties, keywords and units and stick with it. Validators accept the other forms, but a consistent style is easier to read and search.
14. Shorthand value-count errors
Shorthands accept a limited number of values, and the position of each value has a meaning.
/* Before */
.a { margin: 1px 2px 3px 4px 5px; } /* five values */
.b { padding: 10px 20px 30px 40px 50px; }
.c { border: 1px solid red blue; } /* two colours */
.d { font: 16px; } /* font needs a family */
/* After */
.a { margin: 1px 2px 3px 4px; } /* top right bottom left */
.b { padding: 10px 20px 30px 40px; }
.c { border: 1px solid red; }
.d { font: 16px sans-serif; }
Box shorthands such as margin, padding and border-width take one to four values. One value applies to all four sides. Two values mean vertical, then horizontal. Three values mean top, horizontal, then bottom. Four values go clockwise from the top. Five is always an error, and my css-tree run reported the five-value padding as a mismatch.
The font shorthand is stricter than most. It requires at least a size and a family, in that order. Setting font: 16px alone is invalid. If you only want to change the size, use font-size.
Another hidden problem is that valid shorthands reset related longhands. background: red also resets background-image, background-position and others to their initial values. That is not a validation error, but it explains why a line that passes the validator can erase your earlier work.
15. Invalid custom property use
Custom properties (CSS variables) are checked loosely when declared, because the validator does not know where you plan to use them. Problems usually appear at use time.
/* Before */
:root { -gap: 4px; } /* single hyphen */
.a { padding: --gap; } /* missing var() */
.b { margin: var(--missing); } /* never defined */
.c { width: var(--w 10px); } /* malformed var() */
/* After */
:root { --gap: 4px; }
.a { padding: var(--gap); }
.b { margin: var(--missing, 8px); } /* fallback value */
.c { width: var(--w, 10px); }
A custom property name begins with two hyphens. A reference must go through var(). There is a subtle rule here: if a var() points to a property that does not exist and has no fallback, the declaration is valid at parse time but becomes "invalid at computed-value time". The browser then behaves as if the value was unset. In my test, margin: var(--missing) produced a margin of 0px, and border-left: 5px solid var(--missing2, green) correctly used its green fallback. No validator can catch the first case in general, because the value of a custom property can be set from anywhere, including JavaScript.
16. @import ordering
@import rules must come before every other rule except @charset and @layer statements that declare names. An @import placed after normal rules is ignored.
/* Before */
body { margin: 0; }
@import url("theme.css");
/* After */
@import url("theme.css");
body { margin: 0; }
Validators report a misplaced @import, which is the kind of mistake that is awkward to find otherwise, because the only symptom is that a whole file of styles never arrives. There is also a performance reason to avoid @import in production: the browser cannot discover the imported file until it has fetched and parsed the first one, which serialises downloads. Linking stylesheets from the HTML, or bundling them, avoids that delay.
17. calc() missing spaces around + and -
Inside calc(), the + and - operators must be surrounded by whitespace. * and / do not need spaces, but adding them is clearer.
/* Before */
.a { width: calc(100%-20px); }
.b { width: calc(100% -20px); }
.c { height: calc(10px+5px); }
/* After */
.a { width: calc(100% - 20px); }
.b { width: calc(100% - 20px); }
.c { height: calc(10px + 5px); }
The reason is the tokeniser. -20px is a single number token, so 100% -20px is a percentage followed by a number with no operator between them. And 100%-20px has no valid reading at all. In my browser test, CSS.supports("width", "calc(100%-20px)") returned false and calc(100% - 20px) returned true, and a rule using the unspaced version left the element at its default width. Stylelint's function-calc-no-unspaced-operator flagged both sides of the operator on my sample.
18. Duplicate properties, and when they are intentional
Duplicate properties inside one rule are often a mistake: two people edited the same rule, and the second declaration silently wins. Stylelint reported Duplicate property "color" on my sample. The W3C validator does not usually treat duplicates as errors, because they are valid CSS.
Sometimes the duplicate is deliberate. It is the oldest progressive enhancement technique:
.hero {
background: #1a4d8f; /* fallback for any browser */
background: linear-gradient(#1a4d8f, #0b2a52);
color: #fff;
color: color-mix(in srgb, white 92%, #1a4d8f);
}
A browser that does not understand the second background or color declaration drops it, and the first one stays in effect. This works because of the error recovery rules above, and in my test a valid color: red followed by color: nonsense stayed red. If your stylelint configuration forbids duplicates, either allow them for properties where you rely on fallbacks, or use a feature query (@supports) instead, which makes the intent explicit.
.hero { background: #1a4d8f; }
@supports (background: linear-gradient(red, blue)) {
.hero { background: linear-gradient(#1a4d8f, #0b2a52); }
}
Be careful with fallbacks when the newer syntax is something a browser parses but cannot do. The fallback pattern only protects you against values the browser considers invalid, not values it accepts and handles badly.
Modern features and validator lag
CSS has grown fast. Features that were experimental a few years ago are now ordinary: nesting, cascade layers, container queries, the :has() selector, color-mix(), relative colour syntax, and range syntax in media queries. A validator needs a grammar for each of these, and keeping a validator current is volunteer-heavy, specification-tracking work. Validators can lag behind the specifications, sometimes for a long time.
Here is what I observed, which should be read as one data point, not a guarantee:
- In Chrome 153,
CSS.supportsreturned true forcolor-mix(in srgb, red, blue), forselector(:has(a))and forselector(:is(a)). I did not render the nesting or@layersamples in the browser, so treat those as unverified. - With
css-tree3.2.1, a sample file usingcolor-mix(), nested&:hover,@layer,@containerand:has()produced no parse errors and no value errors. - With Stylelint 18.0.0 and the rules I enabled, the same sample produced only two stylistic complaints about formatting, and none about unknown features.
I did not run the online W3C service, since the sandbox has no network access. Its behaviour for new features depends on which profile you select and on the version deployed, and I cannot say what it reports today. If you validate a stylesheet that uses recent features and get an error for a construct that works in current browsers, check the following, in order:
- Select the right profile. The W3C service lets you choose a CSS profile. A stricter or older one rejects newer syntax.
- Check the feature on MDN. The compatibility table tells you whether the syntax is standard and where it works. See MDN's CSS reference.
- Test in a browser.
CSS.supports()in the console answers the practical question directly. - Treat the message as a possible false positive. If a feature is in a current specification and works in browsers you support, a validator error does not mean your CSS is wrong. It may mean the validator has not caught up.
The opposite problem exists as well. A validator that has not caught up will not flag a feature you use incorrectly, so a clean result on modern syntax is weaker evidence than a clean result on classic syntax. Combine validation with a browser check when you use recent features.
Plan for support separately. A valid use of :has() in a browser that does not understand it will drop the whole rule, by rule 2. Use @supports selector(:has(a)) to guard code that cannot degrade gracefully.
Real-world use cases
Validation is not a ritual you perform before launch. It pays off in a handful of concrete situations.
Debugging a rule that does nothing. When a style has no effect and the specificity looks right, a validator reports whether the browser even kept the declaration. This is faster than guessing.
Auditing an inherited stylesheet. On a legacy project, a validation pass finds dead declarations, misspelled properties and stray syntax that have been silently ignored for years. Fixing them changes nothing visible, and then no longer hides real problems.
Reviewing generated or third-party CSS. Code from a design tool, a CMS plugin or a copy-pasted snippet may include invalid lines. Check it before it ships in your bundle.
Checking output from a preprocessor. Sass, Less and PostCSS can emit unexpected output, particularly when variables are interpolated into the wrong place. Validate the compiled CSS, not just the source.
Preparing CSS for email or embedded environments. Some email clients and webviews are strict or unforgiving about syntax. Clean CSS lowers the risk.
Learning. A validator is a patient teacher. Pasting a snippet and reading the message for each mistake teaches the grammar of each property.
Before minification. Minifiers reformat and compress your code. Invalid input gives unpredictable output, so validate first. Our guide to CSS minification and critical CSS explains what a minifier changes.
Step-by-step: validate CSS with DevUtilX
For a quick check without installing anything, use the DevUtilX CSS Validator. It runs in your browser, which means the stylesheet you paste is processed locally and not uploaded anywhere. That matters when the CSS belongs to an unreleased product.
- Open the CSS Validator tool.
- Paste your stylesheet, or a single rule, into the input editor.
- Run the validation.
- Read the list of reported problems. Each one should include a position so you can find it.
- Fix the first problem in your source, then validate again. Fix errors in order from the top, because one early mistake, such as a missing brace, can cause a cascade of reports that disappear when it is fixed.
- Repeat until the result is clean, then copy the corrected CSS back into your project.
If you validate a large file and get a long list, do not try to fix everything in one go. Start with syntax errors (braces, semicolons, comments), since they can distort everything below them. Then work through unknown properties and invalid values. Treat warnings last: vendor-prefix notices and similar messages are often deliberate.
A tip for isolating a problem: paste only the rule that misbehaves, with its closing brace. If the rule validates in isolation but fails in the full file, the cause is an earlier unclosed block or comment. For a visual comparison of the file before and after your fixes, our guide to comparing files and code diffs shows how to review changes line by line.
Validating CSS in code
The tool is the fastest route for one-off checks. For repeated checks, put the same logic in a script. These examples were all run in the sandbox before being included here.
JavaScript with css-tree
css-tree parses CSS into a syntax tree and ships a lexer that knows the property grammars. This script collects parse errors, then checks each declaration's value.
const csstree = require("css-tree");
const css = require("fs").readFileSync(process.argv[2], "utf8");
const errors = [];
const ast = csstree.parse(css, {
positions: true,
onParseError: (e) => errors.push(`${e.line}:${e.column} ${e.message}`),
});
console.log("parse errors:");
errors.forEach((e) => console.log(" " + e));
console.log("value check:");
csstree.walk(ast, {
visit: "Declaration",
enter(node) {
if (node.property.startsWith("--")) return; // custom properties
const match = csstree.lexer.matchDeclaration(node);
if (match.error) {
console.log(` line ${node.loc.start.line}: ${csstree.generate(node)} -> ${match.error.message.split("\n")[0]}`);
}
},
});
The output on the sample from earlier is the list shown in the section on how a validator reads your stylesheet. Skipping custom properties matters, since their values are free-form.
JavaScript with Stylelint's API
Stylelint is a linter, which we compare with validation below, but its rules for unknown properties, units and colours cover most of what a validator does, and it runs locally with a non-zero exit code. This script is suitable for a CI job.
import stylelint from "stylelint";
const result = await stylelint.lint({
files: process.argv.slice(2),
config: {
rules: {
"property-no-unknown": true,
"unit-no-unknown": true,
"color-no-invalid-hex": true,
"declaration-block-no-duplicate-properties": true,
},
},
});
for (const r of result.results) {
for (const w of r.warnings) {
console.log(`${r.source.split("/").pop()}:${w.line}:${w.column} ${w.text}`);
}
}
process.exit(result.errored ? 1 : 0);
On a file containing colour: red, width: 10pxx, background: #12345, and two color declarations, it printed these lines and exited with status 1:
ci-bad.css:4:15 Invalid hex color "#12345" (color-no-invalid-hex)
ci-bad.css:5:3 Duplicate property "color" (declaration-block-no-duplicate-properties)
ci-bad.css:2:3 Unknown property "colour" (property-no-unknown)
ci-bad.css:3:12 Unknown unit "pxx" (unit-no-unknown)
On a clean file, it printed nothing and exited with 0. The order of messages comes from the rules, not the line numbers, so sort them if you need a tidy report.
Python: a structural pre-check
Python has no standard CSS parser, and I did not want to depend on a library I had not tested. This small script checks the structural mistakes that damage the most code: unclosed comments, unbalanced braces, // comments and = instead of :. It does not check property names or values, so treat it as a quick pre-check rather than a validator.
import re, sys
def check(css: str):
problems = []
if css.count("/*") > css.count("*/"):
problems.append("unclosed /* comment")
stripped = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
depth = 0
for lineno, line in enumerate(stripped.splitlines(), 1):
if line.lstrip().startswith("//"):
problems.append(f"line {lineno}: '//' is not a CSS comment")
depth += line.count("{") - line.count("}")
if depth < 0:
problems.append(f"line {lineno}: extra closing brace")
depth = 0
if re.match(r"\s*[a-z-]+\s*=\s*[^;]+;", line):
problems.append(f"line {lineno}: '=' instead of ':'")
if depth > 0:
problems.append(f"{depth} unclosed '{{'")
return problems
if __name__ == "__main__":
css = open(sys.argv[1], encoding="utf-8").read()
found = check(css)
for p in found:
print(p)
sys.exit(1 if found else 0)
Run against a file that started with // reset styles, had a border = 1px solid; line, and ended with an unclosed comment and block, it printed:
unclosed /* comment
line 1: '//' is not a CSS comment
line 4: '=' instead of ':'
1 unclosed '{'
and exited with status 1. For a clean file the exit status was 0. The limits are real: braces inside strings or url() values would confuse the counter, and the script cannot know what the content means. That is the reason to use a real parser for anything important.
Linting versus validation
People use the words interchangeably, but they answer different questions.
Validation asks: is this legal according to the specification? The answer is mostly objective. A property either exists or it does not.
Linting asks: does this follow our rules? A linter such as Stylelint enforces both correctness rules, such as unknown properties and invalid hex colours, and style rules, such as indentation, quote style, ordering and naming patterns. Many of the rules are opinions, and teams choose which to enable.
The practical differences:
| Aspect | Validation | Linting |
|---|---|---|
| Source of truth | Specifications | Your team's configuration |
| Typical output | Errors and warnings about legality | Errors about legality and style |
| Configurable | Barely, profile only | Heavily |
| Catches misspelled properties | Yes | Yes, with the right rule |
| Catches style inconsistency | No | Yes |
| Can auto-fix | No | Often, with --fix |
| Runs well in CI and editors | Possible | Designed for it |
They overlap in the middle, and they complement each other. A validator is an independent check based on the standard. A linter keeps the team consistent and catches problems as you type. In my Stylelint run with the standard configuration plus a few extra rules, 14 problems were reported on a short sample, including unknown properties, an invalid hex colour, an unknown pseudo-class, an unknown unit, an unknown at-rule and an unknown media feature. Two of those were formatting complaints about blank lines before at-rules, which is a style rule. Note that some rules in a linter can themselves be out of date: always check a surprising report against the specification before you change working code.
For style guidance that goes beyond correctness, see our guide to CSS formatting and architecture, which discusses how to organise rules and keep a stylesheet maintainable.
Validating CSS in CI
Catching errors during review is better than finding them in production, and automation makes the check cheap. A reasonable pipeline has three layers.
Layer 1: editor feedback. Install a Stylelint extension so that unknown properties and syntax errors are underlined as you type. This catches most mistakes before they are committed.
Layer 2: pre-commit hook. Run the linter on changed files. A short check on staged files keeps the commit fast.
Layer 3: CI job. Run the same checks on the whole repository, as the source of truth. The job fails when the tool exits with a non-zero status, which stops the merge.
A minimal package script and CI step might look like this:
{
"scripts": {
"lint:css": "stylelint \"src/**/*.css\"",
"test": "npm run lint:css"
}
}
# Excerpt from a CI workflow
- run: npm ci
- run: npm run lint:css
Some practical points make these checks pleasant rather than annoying:
- Validate the compiled output as well as the source when you use a preprocessor, since interpolation can generate invalid values that look fine in the source.
- Decide which rules fail the build. Make unknown properties, invalid values and syntax errors blocking. Make stylistic concerns warnings at first, and tighten them later.
- Set a baseline for legacy code. If an old stylesheet has hundreds of problems, do not block the team. Fix the worst categories first, or lint only changed files until the backlog shrinks.
- Pin tool versions. A new release of a linter can add rules and change results. Update on purpose, not by accident.
- Do not depend on a remote service. A CI job that sends your code to a public validator can fail for network reasons, and it may leak unreleased work. Run an offline tool in your pipeline, and use the browser-based validator for ad-hoc checks.
- Review fixes with a diff. Automated
--fixchanges can be surprising, so inspect them. See comparing files and code diffs.
The same approach works for other languages. If your project also contains markup and scripts, see our guides to HTML formatting and semantic markup and JavaScript code formatting best practices.
Performance, security and accessibility considerations
Validation does not cover these areas directly, but invalid CSS touches all three.
Performance
Invalid CSS is not free. The browser still downloads it, tokenises it and then throws part of it away. Dead declarations add bytes and parse time with no benefit. Cleaning them up shrinks the file, and so does minification. After validation, run your stylesheet through a minifier and compare sizes, as described in CSS minification and critical CSS.
Two validation findings have a performance angle. A misplaced @import is ignored and is also a sign of a loading pattern that delays rendering. And an unclosed block that swallows later rules can force you to add duplicate rules elsewhere to compensate, so the stylesheet grows.
Security
Validation is not a security check, but a few points matter when CSS comes from outside your team.
- User-supplied CSS is untrusted input. Even valid CSS can load external resources through
url(), which can leak that a page was viewed, and attribute selectors can be used to probe values on the page. If your product lets users submit styles, sanitise them with an allowlist of properties, not by checking that they are valid. - Third-party stylesheets can change what your page displays. Load them from sources you trust, and consider Subresource Integrity for files served from a CDN.
- Do not paste private CSS into remote validators unless you are comfortable with the privacy terms. A browser-based tool that processes everything locally avoids the question, which is how the DevUtilX validator works.
Accessibility
A validator will accept CSS that harms accessibility, so check these separately:
- Colour contrast. Valid colours can still be unreadable. Test text against its background.
- Focus styles.
outline: noneis valid and harmful unless you provide a visible replacement. - Motion. Animations are valid but can trigger problems for some users. Respect
prefers-reduced-motion. - Text size and spacing. Fixed pixel sizes can interfere with user zoom and font settings. Prefer relative units where you can.
- Hidden content.
display: noneremoves content from the accessibility tree, while visually hidden techniques keep it available to screen readers. Pick the one that matches your intent.
A bonus: some validation errors cause accessibility failures indirectly. A dropped :focus-visible rule, because of a misspelled selector in a list, removes your focus indicator for everyone.
Troubleshooting guide
When CSS does not do what you expect, work from the symptom to the cause.
Symptom: one declaration has no effect, others in the same rule work
Cause. The declaration is invalid, so the browser dropped it. Common reasons are a missing unit, a misspelled property or value, a bad colour or a mistyped !important.
Fix. Validate the rule, or open the browser's developer tools. Invalid declarations are typically shown struck through or flagged in the Styles pane. Correct the value, and re-test.
Symptom: a whole rule has no effect
Cause. One selector in the list is invalid, or the rule follows a // comment line, or the block above it was never closed.
Fix. Split a comma-separated selector list into separate rules to find the offender. Check the line before the rule for a stray //, and check the previous rule's braces.
Symptom: everything after a certain point stopped working
Cause. An unclosed comment, or a missing closing brace that turns the rest of the file into a nested block.
Fix. Bisect. Comment out the lower half of the file and see whether the upper half behaves, then narrow the range. A validator that reports an error near the end of the file usually points to an unclosed construct earlier.
Symptom: two properties vanished after I edited one line
Cause. A missing semicolon merged two declarations into one invalid declaration.
Fix. Add the semicolon. Enable a lint rule that requires a semicolon on the last declaration to prevent the next accident.
Symptom: the validator reports an error for a feature that works in browsers
Cause. The validator does not yet support the syntax, or the selected profile is too old.
Fix. Select a newer profile if one exists, confirm the syntax on MDN, and verify with CSS.supports() in a current browser. If it is truly valid, record the exception in a comment and ignore that message. Do not rewrite working code to please a tool.
Symptom: the stylesheet validates but the page looks wrong
Cause. Valid CSS can still have the wrong selector, the wrong specificity, a typo in a class name or a conflict with another rule. The validator does not match selectors against your HTML.
Fix. Use the browser's inspector to see which rules apply and which are overridden. Compare class names with the markup. Our guide to HTML formatting and semantic markup can help you check that the document structure is what your selectors expect.
Symptom: the property works in one browser but not another
Cause. A browser-support difference, not a syntax problem. Validation passes because the syntax is legal.
Fix. Check the compatibility table, add a fallback declaration before the new one or guard it with @supports.
Symptom: margin: var(--x) produces no margin
Cause. The custom property is undefined, or the value it holds is invalid for margin. The declaration is invalid at computed-value time and falls back to the initial or inherited value.
Fix. Define the variable in a scope that reaches the element, give var() a fallback, and check the value you assign to it.
Symptom: the @import has no effect
Cause. It appears after other rules.
Fix. Move it to the top of the file, or link the stylesheet from the HTML instead.
Symptom: a rule works until the file is minified
Cause. The minifier changed something that depended on lenient parsing, or the source contained a construct that was only accidentally valid, such as calc() with missing spaces.
Fix. Validate the source and the minified output separately. Fix the source, and avoid relying on whitespace quirks.
Best practices and checklist
Prevention is cheaper than debugging. These habits remove most of the problems in this guide.
- End every declaration with a semicolon, including the last one in a block.
- Use an editor with brace matching and CSS highlighting. An unclosed brace shows up as a colour change.
- Use
/* ... */comments only, even if you work with a preprocessor. - Quote font names that contain spaces or digits. Never quote generic families.
- Always include units on lengths, except for zero.
- Put vendor-prefixed selectors in their own rules.
- Put fallback declarations before the new value, and use
@supportswhen the intent needs to be obvious. - Space the operators in
calc(). - Give
var()a fallback where an undefined variable would break the layout. - Place
@importfirst, or avoid it in production. - Lint on save and in CI, and fail the build on unknown properties and invalid values.
- Validate the compiled output when you use a preprocessor.
- Verify modern features in a browser because validators lag.
- Remove dead and duplicate declarations unless they are deliberate fallbacks, in which case label them with a short comment.
- Format consistently. A formatted file is easier to read, and mistakes stand out. See CSS formatting and architecture.
A short pre-release checklist:
- Does the stylesheet validate without errors, using the right profile?
- Have you reviewed each warning and decided whether it is intentional?
- Does the linter pass in CI?
- Have you tested features that validators may not know in the oldest browser you support?
- Is there no
//comment,=declaration or unclosed block left in the source or in the compiled output? - Have you checked contrast, focus and reduced motion by hand?
- Is the production file minified and served compressed?
Comparison: ways to check your CSS
| Approach | What it finds | Strengths | Limits |
|---|---|---|---|
| Browser-based validator (DevUtilX CSS Validator) | Syntax errors, unknown properties, invalid values | No setup, local processing, immediate | Manual, grammar coverage depends on the tool |
| W3C CSS Validation Service | The same kinds of problems, plus warnings | Independent and long established | Needs network access, may lag on new features |
| Stylelint | Errors and style rules, with fixes | Configurable, fits editors and CI | Rules are opinions, needs configuration |
| Parser libraries (css-tree and similar) | Parse errors and grammar mismatches programmatically | Scriptable, custom reports | You write the glue, coverage varies |
| Browser developer tools | What the browser actually applied | Shows real behaviour, flags dropped declarations | Per page, manual |
CSS.supports() |
Whether one browser accepts a value | Direct answer for that browser | One browser at a time |
| Visual and regression testing | Layout and rendering problems | Catches what grammar cannot | Setup effort, does not explain why |
No single row covers everything. A sensible setup combines a linter or validator for correctness with browser tools for behaviour.
FAQ
Does CSS validation guarantee that my page looks right?
No. It confirms that the syntax and values are legal according to a specification. It does not check browser support, whether selectors match your HTML, colour contrast or layout.
What happens when a browser finds invalid CSS?
It drops the invalid part and keeps going. An invalid declaration is ignored while the rest of the rule applies. An invalid selector discards the whole rule. A broken at-rule is skipped with its block.
Why did one typo disable my whole selector list?
Selector lists are parsed as a unit. In my test, a rule with a valid .t2 selector and an invalid .t3:hovr selector applied to neither. Put selectors that some browsers may not understand into separate rules.
Is it an error to use vendor prefixes?
Not in browsers. Validators usually flag prefixed properties as warnings about vendor extensions, since they are not part of the standard grammar. Keep the ones you need, and let a tool such as Autoprefixer manage them.
Why does the validator complain about new features that work in my browser?
Validators track the specifications with a delay, and the profile you choose affects which syntax is accepted. Confirm the feature on MDN and in a browser, and treat the message as a possible false positive.
Are duplicate properties an error?
They are valid CSS. They are often a mistake, but they are also a deliberate fallback technique, where an older value comes first and a newer value follows. A browser that does not understand the new value drops it.
Do I need quotes around font names?
Quote any family name that contains spaces or digits, as a habit. A name made of plain identifiers, such as Open Sans, is legal unquoted, but a name like Font 2 is invalid without quotes. Do not quote generic families such as serif.
Why is margin: 10 invalid?
Lengths other than zero need a unit. The browser dropped that declaration in my test and applied no margin. Write 10px, 1rem or another unit.
Can I use // comments in CSS?
No. Only /* ... */ is a comment in CSS. A // line becomes part of the next selector and invalidates the rule that follows. Preprocessors such as Sass accept // and strip it when compiling.
What is the difference between a linter and a validator?
A validator checks legality against the specification. A linter checks legality plus your team's style rules, and can often fix problems automatically. They work well together.
Should validation run in CI?
Yes, ideally with an offline tool such as Stylelint so that the build does not depend on a remote service. Fail the build on syntax errors, unknown properties and invalid values, and treat style issues as warnings at first.
Is my CSS uploaded when I use the DevUtilX validator?
No. The tool processes your input in your browser. Even so, avoid pasting secrets, since CSS can contain URLs with tokens.
Does valid CSS load faster?
Not by itself. Removing dead declarations reduces size slightly, but real gains come from minification, compression, caching and loading only the CSS a page needs.
Does a missing closing brace always break the rest of the file?
In my test with a current browser, the rules after the unclosed block were read as nested rules and stopped matching their intended elements. Older browsers recover differently. In every case the safest assumption is that everything after the mistake is at risk.
Conclusion
CSS fails quietly by design, and that makes the quiet failures easy to miss. Validation turns them into visible messages: a missing semicolon, a unit that was never typed, a selector that dropped a whole list, a // comment that disabled the rule after it. Learn the three recovery rules, keep your editor and linter on, validate in CI, and check modern features in a real browser, since validators lag. Remember that a clean result is a statement about syntax only. Rendering, support, performance and accessibility still need their own checks.
To try it on your own stylesheet, paste it into the DevUtilX CSS Validator. It runs entirely in your browser. For related reading, see our guides on CSS formatting and architecture and CSS minification and critical CSS.

