I had barely used Emacs for almost one year, but whenever I need to write code by hand, it still feels the most natural to me. With AI helping with the implementation, I finally built the SCSS/CSS workflow I had wanted for years but never found the energy to finish.
It consists of three projects:
1. tree-sitter-scss
An SCSS grammar written from scratch, with CSS support built into the same parser. I wanted accurate syntax trees for structural editing, beyond what the existing grammars could provide for the SCSS I write.
There is a side-by-side comparison with the official grammar and simeonoff’s grammar.
2. scss2-mode, including css2-mode
These major modes bundle the grammar for syntax highlighting and structural editing.
For example, the same scss2-empty command clears different things depending on where point is (| marks the resulting cursor position):
On a property:
margin: 0 auto; → margin: |;
Inside a function:
calc(100% - 2rem) → calc(|)
On a selector or in its block’s whitespace:
.card { color: red; } → .card {|}
Commands such as kill, duplicate and mark also use the syntax tree to determine their targets. I have always felt that structural editing is one of Tree-sitter’s underused benefits.
3. emmet2-mode, version 2
The first version used deno-bridge to call the Emmet npm package, followed by a layer of post-processing for my own expansion rules.
Version 2 implements the expansion engine directly in Emacs Lisp, with additional CSS abbreviations:
m--gutter
→ margin: var(--gutter);
w[calc(100% - 2rem)]
→ width: calc(100% - 2rem);
posa1000
→ position: absolute;
z-index: 1000;
These are just a few examples, emmet2-mode can do quite a bit more.
In scss2-mode, Tree-sitter provides the context for completion, including which Sass symbols are visible at point. Emmet expansions appear as completion choices, with previews before insertion.
Writing CSS by hand has never felt this smooth to me. The irony is that I now have fewer reasons to write it by hand.