CSS has changed a lot.
We now have native nesting, :is(), :where(), :has(), range media queries, cascade layers, container queries, @scope, @starting-style, and much more.
But there is still an awkward problem:
How do you safely move an existing flat CSS codebase toward modern CSS?
Going from nested CSS → flat CSS is mostly compiler lowering. Going in the opposite direction is much harder.
Consider:
.card {
padding: 1rem;
}
.card:hover {
color: blue;
}
.card > .body {
min-width: 0;
}
.card + .card {
margin-top: 1rem;
}
@media (width >= 48rem) {
.card {
padding: 1.5rem;
}
}
A modernized version could be:
.card {
padding: 1rem;
&:hover {
color: blue;
}
> .body {
min-width: 0;
}
+ .card {
margin-top: 1rem;
}
@media (width >= 48rem) {
padding: 1.5rem;
}
}
That example looks simple. Real stylesheets aren't.
Once you introduce selector lists, pseudo-elements, :is(), :not(), cascade layers, !important, non-adjacent rules, comments, duplicate fallback declarations, @scope, container queries, and source-order dependencies, a refactoring that merely looks correct can subtly change the cascade.
That is why I built CSSForge.
CSSForge is a safety-first semantic CSS refactoring and modernization engine written in Rust. It includes both a headless CLI and an interactive terminal workbench.
The core idea: prove before transforming
CSSForge does not treat CSS as text replacement.
Before applying a transformation, its proof model checks things such as:
- selector/specificity equivalence
- declaration preservation
-
!importantpreservation - source-order/cascade equivalence
- at-rule context
- cascade-layer precedence
- scope boundaries
- lossless source editing If a transformation cannot be proven safe, the goal is to refuse it instead of producing prettier but semantically different CSS.
For example:
.alpha .title,
#hero .title {
color: red;
}
may look like a good candidate for:
:is(.alpha, #hero) .title {
color: red;
}
but :is() uses the specificity of its most specific argument. That can lift the .alpha branch to ID-level specificity.
CSSForge's design explicitly treats specificity inflation as something to reject rather than hide.
Another common trap:
.card {}
.card__title {}
This is not valid native-CSS nesting:
.card {
&__title {}
}
Native & is a selector, not Sass-style string concatenation. CSSForge explicitly refuses that transformation as well.
More than basic :hover nesting
The current ruleset covers 26 transformations across several groups, including native nesting, conditional at-rules, modern selector transformations, at-rule merging, deduplication, and structural refactoring.
Examples include:
Nesting
nest-pseudo-classnest-pseudo-elementnest-attributenest-compoundnest-descendantnest-combinator-
factor-selector-listConditional at-rules nest-medianest-supportsnest-container-
nest-starting-styleModern selectors modernize-ismodernize-where-
modernize-media-range-syntaxMerging & deduplication merge-same-named-layermerge-adjacent-mediamerge-adjacent-supportsmerge-adjacent-containermerge-adjacent-identical-selectormerge-identical-rule-bodiesfactor-identical-states-with-is-
gather-related-selector-rules## Lossless editing matters
One thing I specifically did not want was:
parse entire stylesheet
→ serialize entire stylesheet
→ generate a massive formatting diff
CSSForge's documented architecture uses semantic parsing for validation but applies changes through targeted byte-range patches, so untouched source can remain byte-for-byte unchanged, including comments, indentation, and quote style.
That matters when reviewing a real PR. A refactoring tool should ideally show you:
.card {
padding: 1rem;
+
+ &:hover {
+ color: blue;
+ }
}
-
-.card:hover {
- color: blue;
-}
—not a 5,000-line formatting diff.
Interactive TUI + CLI
For interactive work:
cssforge
# or
cssforge interactive ./src/css
The TUI lets you select files and rules, cycle presets, and inspect unified diffs before applying changes.
For automation:
cssforge analyze ./src
cssforge analyze ./src --json
cssforge apply ./src/app.css \
--preset modern \
--output new-file
Install with Cargo:
cargo install cssforge
Precompiled builds are also documented for Linux x64/ARM64, Windows x64/ARM64, and Apple-Silicon macOS.
What CSSForge is not
CSSForge is not trying to replace:
- a formatter
- a minifier
- Stylelint
- Lightning CSS
- PostCSS
- your build pipeline The narrower goal is:
Forward semantic CSS refactoring: modernize only where the transformation can be justified, and preserve the original code where it cannot.
That includes deliberately refusing BEM string concatenation, specificity-lifting transformations, destructive reserialization, and arbitrary at-rule movement across cascade barriers.
Open source
CSSForge is written in Rust and released under the MIT license. The workspace includes the main CLI/TUI executable, a reusable core library, and a Ratatui-based TUI crate.
If you work with large CSS codebases, I'd especially like feedback on:
- CSS patterns that are difficult to refactor safely
- selector edge cases
- cascade/specificity traps
- additional transformations worth proving
- real-world stylesheets that make good stress tests If this problem interests you, give the repo a try and let me know where it breaks.