Core Concepts
CSS generation
What the compiler produces, and why.
One expression, one rule
<div class="md:hover:bg(brand)"></div>:root {
--croff-color-brand: #c8fd12;
}
@media (min-width: 768px) {
.md\:hover\:bg\(brand\):hover {
background-color: var(--croff-color-brand);
}
}The selector is the expression itself, escaped. Your markup isn't rewritten, and no hashes are involved, so DevTools shows what you wrote and any tool that renders a class attribute works.
Only what you use
Rules are generated from expressions found in your source. Nothing else is. The same goes for theme variables.
Order
Rules are sorted by a fixed scheme, independent of where expressions appear:
- Theme variables, by scale and then token order.
- Rules without at-rules, then breakpoints narrowest first, then other at-rules. Rules that share an at-rule share a block.
- State variants, then utility registry order, then the expression's text.
Two builds of the same source are byte-identical, and tests hold the compiler to this by shuffling its input.
Order only settles ties. Specificity comes first, so hover: beats md: even though it's earlier. See How conflicts are decided.
Escaping
Selectors follow the CSS.escape algorithm, including the unusual cases: 2xl: starts with a digit, so it becomes .\32 xl\:....
The directive
@croff; expands to theme variables and utilities. You can place each part separately:
| Directive | Output |
|---|---|
@croff; | Theme variables, then utilities. |
@croff reset; | A small reset, which isn't included otherwise. |
@croff theme; | Theme variables only. |
@croff utilities; | Utilities only. |
Minification
Generated CSS is printed compactly when minifying, instead of being post-processed. The CLI also strips comments and whitespace from your own CSS around the directive, and with Vite your bundler's minifier handles it.
Source maps
croff build --sourcemap maps each rule to the first place its expression is used, so that your browser can take you from a rule to the markup that needs it. See Debugging.
