rust-cssparser: the CSS Syntax Level 3 tokenizer, and where it deliberately stops
Rust implementation of CSS Syntax Level 3
At a glance
- What is it?
- The cssparser crate is Servo's Rust implementation of the CSS Syntax Module Level 3, and its most useful property is where it ends: it tokenizes and builds a tree of component values, and it stops there rather than interpreting selectors or properties. It assumes UTF-8, does not materialize raw tokens, and ships as a workspace with a color crate and a macro crate.
- Who is it for?
- Adopt cssparser when you need a spec-accurate CSS tokenizer that hands you component values and lets you decide what a selector or a property means, particularly if you are writing a linter, a minifier or a browser-adjacent tool. Do not adopt it expecting a full CSS parser, since it stops short of selectors and properties by design.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 15 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the crate covers, and the step it leaves out
cssparser is a Rust implementation of the CSS Syntax Module Level 3, and it is the parsing layer of Servo's web engine stack. The README points at Servo's style crate as an example of a parser built on top of it, which is the clearest statement of what this crate is for.
The README describes CSS parsing as a series of steps, and the crate's boundaries fall right in the middle of that list.
The first step is decoding. When parsing from bytes, reading a file or fetching a URL, a parser detects the character encoding from the Content-Type header, an @charset rule or a byte order mark, and decodes to Unicode text. The README is explicit that rust-cssparser does not do this yet and just assumes UTF-8. This step is skipped when parsing from Unicode, for example inside an HTML style element.
The second step is tokenization, also called lexing, transforming the input into a stream of tokens. The README notes that tokenization never fails, although the output may contain error tokens.
The third step turns that flat token stream into a tree of component values, which are either preserved tokens or blocks and functions that contain more component values. Here rust-cssparser does something specific: it performs this step at the same time as tokenization, so raw tokens are never materialized and you only ever get component values.
The fourth step, parsing the remaining component values into selectors, specific CSS properties and so on, is not done. The README says by design it does not do this last step, since it depends a lot on what you want to do, which properties you want to support and what you want to do with selectors.
Why skipping the last step is the right design for a crate
It is worth spending a paragraph on why a crate that stops early is more useful rather than less.
The final step of CSS parsing is where opinions live. A browser needs to know about every property in the spec plus every vendor extension plus whatever the page author invented. A linter needs to know the property names and their value grammar to report meaningful errors. A minifier needs to know which values can be safely shortened and which whitespace is required. A server-side renderer needs to compute values. None of those is the same set of knowledge, and a library that committed to one of them would be wrong for the others.
By stopping at component values, cssparser commits only to the part of CSS that is settled: the syntax. That part is specified precisely, it does not change with the value of a property, and it is the same for every consumer.
The README then notes that the crate does provide some helper functions to parse CSS colors and An+B, the argument to :nth-child() and related selectors. Those two are the exceptions, and they are instructive. Colors are ubiquitous enough that parsing them by hand is repetitive, and An+B appears in enough selector contexts that a bug in it would be repeated everywhere. Everything else is left to the caller.
The result is a crate where the hard, spec-shaped, bug-prone work is done and the interpretive work stays with the consumer. For a project that only needs to recognise and skip at-rules, that is all you need. For a project that needs to reason about declarations, the tree of component values gives you the structure to do it on.
The data model: component values, rules and declarations
After the component value tree exists, the next level is generic rules and declarations. The README describes this stage as the header and body of rules, along with the value of declarations, still being lists of component values at that point, and points at the Token enum in src/tokenizer.rs as the place to look at the data structure.
So the shape is a tree with two levels above the leaves. A rule has a header, which is a prelude of component values, and a body. A declaration has a name and a value, where the value is a list of component values that has not yet been interpreted. Whether a block contains rules or declarations is decided later, by the consumer, based on context.
The preserved-token versus function distinction carries a lot of weight in that model. A preserved token is a token that the parser had to keep as-is because it appeared in a position where it could not be consumed structurally, such as inside a function. Everything else has been absorbed into the block structure. That is why the tree is complete enough to reprint a stylesheet faithfully, and why a minifier built on this crate can reproduce the input when it does not actually want to change anything.
The error handling follows from the same philosophy. Tokenization never fails, so an unparseable sheet is not an error case in the API; it produces error tokens inside an otherwise well-formed tree. The consumer decides whether to complain about them, ignore them, or propagate them. That is a considerably easier contract to build a browser on top of than one where every parse call can fail.
The crate structure: a workspace with color and macro sub-crates
The Cargo.toml describes a workspace rather than a single crate, with members at the root, at ./macros and at ./color, and the same three as the default members.
The macros crate is the cssparser-macros package, an optional dependency at version 0.7.0 that provides a procedural macro. It is what the fast_match_byte feature enables, and that feature is on by default along with fast_match_color. The macro approach is the point: the hot path in a tokenizer is comparing a byte against a set of expected characters, and a generated match table beats a runtime lookup, so the crate pays for a build-time macro to make the common case fast.
The color crate is the workspace's other member, and fast_match_color pulls in phf with the macros feature. A perfect hash map is the right structure for the named colour keyword table, since lookups happen on every colour value and the table never changes.
The dependency list outside those is short and tells you the crate is self-contained. dtoa-short and itoa handle number to string and string to number conversion, and smallvec handles the small vectors that component value lists produce. The optional dependencies add malloc_size_of for size accounting, serde with derive for serialisation, and the macros and phf crates behind the two default features.
The package metadata gives the version as 0.38.0, the edition as 2024 and the minimum Rust version as 1.85. The author field credits Simon Sapin, who wrote the original Rust CSS parser that became the basis for this one. The published crate on docs.rs is cssparser, and the repository URL is the servo organisation.
Features, testing and the fuzzing setup
The feature list is short and its comments explain the reasoning.
fast_match_byte and fast_match_color are the defaults, and each turns on a dependency: the macro crate and the phf crate respectively. The bench feature is an empty marker. skip_long_tests is described as useful for skipping tests when execution is slow, for example under miri. Miri is Rust's undefined behaviour checker, and running a parser's full test suite under it is slow enough that a flag to skip the long cases is a practical necessity rather than a convenience.
The dev dependencies suggest what the tests check. serde_json and the difference crate are for comparing serialised output, and encoding_rs is there because encoding matters for a parser that assumes UTF-8. That last one is a good sign: a parser whose only stated limitation is that it does not detect encodings would want a test that at least confirms the UTF-8 assumption holds for the inputs it is given.
The exclude list in the package metadata is also revealing. It excludes src/css-parsing-tests/ and src/big-data-url.css from the published package. The first is the test corpus, which is not needed by consumers, and the second is a large data URL fixture. Excluding them keeps the published crate small while the repository keeps the material for the test suite.
The repository layout backs this up. There is a fuzz/ directory, which is what you would expect for a parser that must never panic on hostile input, and a macros/ directory for the procedural macro crate, plus color/, docs/, src/ and a Cargo.toml at the root. The fuzzing setup matters more here than in most crates: a CSS parser is routinely handed input written by attackers, since CSS arrives with web pages.
Limits: UTF-8 only, no encoding detection, and a 0.x version number
The limits are worth being concrete about.
The encoding gap is the one the README calls out. Any parser that reads CSS from bytes is supposed to detect the character encoding from the Content-Type header, an @charset rule or a byte order mark. This crate does not do it and assumes UTF-8. The practical consequence is that a caller handling bytes has to do the decoding itself, which means implementing the detection rules correctly, and those rules have their own corner cases. If your input is already Unicode, for example a style element in an HTML document, the problem does not arise.
The second limit is the version number. The package metadata says 0.38.0, and the only tagged release listed for the repository is v0.35.0 from 2025-03-20, while the last push was on 2026-10-08. Under Cargo's versioning rules, a 0.x version carries no compatibility guarantee, so an upgrade can break your code without a major version bump. The gap between the tagged release and the current version also means the crates.io release may lag the repository, and you should check which one you are actually getting.
The third limit is what you have to build yourself. Selectors are not parsed. Properties are not parsed. The only helpers provided are for colours and An+B. If you want to know whether a declaration is valid, or to compute a specificity, or to resolve a var() reference, that is your code. The crate gives you a faithful tree and leaves the interpretation to you, which is the right division of labour for a browser and the wrong one if you wanted a ready-made CSS object model.
The fourth is that the crate is a component, not a product. It has no CSSOM, no cascade, no specificity, no computed values. Depending on it means depending on a parsing layer you will pair with something else.
cssparser against a full CSS object model and against writing a tokenizer yourself
Two comparisons clarify where this crate fits.
The first is against a full CSS object model, a library that parses stylesheets into typed rule and declaration objects with properties understood. The difference in approach is where the type information lives. A full object model has to encode the whole property set into its API, which means it carries the size and the maintenance cost of the CSS property index, and it commits to an opinion about what a property means. cssparser carries only the syntax and lets you layer whatever property knowledge you need on top.
That trade goes the other way in practice for the browser, which is why Servo built its own style crate on top of this one rather than adopting an off-the-shelf CSS object model. A browser needs to compute values, apply the cascade and handle inheritance, and none of that is worth building into the parsing layer. For an application that only needs to read a few properties out of a stylesheet, the object model is the shorter path.
The second comparison is against writing a tokenizer yourself, which is a common exercise and a common mistake. A CSS tokenizer has more edge cases than it first appears: escapes in identifiers, unicode ranges in selectors, comments inside strings, bad url tokens, a closing brace that ends a block early, an unterminated string at the end of a file. The specification pins down the behaviour for each of these, and a hand-written tokenizer usually diverges from the spec in a handful of them, which then produces different behaviour between your tool and every browser.
The argument for using cssparser is therefore not convenience but agreement. A crate that implements the specification, is fuzzed, and is maintained by the Servo project gives you the same parse of the same bytes that a browser gives you. That is the property worth paying for, and it is why the crate is a dependency of a browser rather than a convenience wrapper.
The counterweight is the same one as with any component: you are taking a dependency on a 0.x crate from a project whose primary interest is elsewhere. The mitigations are the ones the repository itself provides, which is that the crate is small, its dependencies are few, and the fuzzing directory shows the input surface is treated adversarially.
Editorial conclusion
Adopt cssparser when you need a spec-accurate CSS tokenizer that hands you component values and lets you decide what a selector or a property means, particularly if you are writing a linter, a minifier or a browser-adjacent tool. Do not adopt it expecting a full CSS parser, since it stops short of selectors and properties by design. Verify first that your input is UTF-8, since the encoding detection step is not implemented, and check whether the helper functions for colors and An+B cover the parts of the grammar you would otherwise have to write.
Frequently asked questions
What does the cssparser crate parse and what does it leave out?
It implements CSS Syntax Level 3: it tokenizes Unicode text into component values, building the block and function tree at the same time so raw tokens are never materialized. It stops before parsing selectors and specific CSS properties, because that depends on what you want to support. Helper functions are provided for CSS colors and An+B.
Does cssparser detect the character encoding of a stylesheet?
No. The README states that rust-cssparser does not do this yet and just assumes UTF-8, where a full parser would detect the encoding from a Content-Type header, an @charset rule or a byte order mark. The step is skipped when the input is already Unicode, such as inside an HTML style element.
Which Rust version and edition does cssparser require?
The Cargo.toml declares edition 2024 and rust-version 1.85, with the package version at 0.38.0. It is a workspace with members at the root, ./macros and ./color, and the default features fast_match_byte and fast_match_color enable the procedural macro and the phf dependency respectively.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/servo-rust-cssparser)Was this useful?