(Posts)

Revisiting SCSS in Emacs: tree-sitter-scss, scss2-mode and emmet2-mode

Oct 9, 2026 [ Sass , Emacs , Tree-sitter ]
show, don't tell https://github.com/P233/tree-sitter-scss

I write a lot of SCSS, and I have always wanted highlighting that covers all of it. In the Sublime Text days I wrote Syntax Highlighting for Sass, a package for Sass and SCSS that was fairly popular at the time. I learned regular expressions while writing it, and by the end I had written far more of them than I ever expected to.

After I moved to Emacs, I never found the energy to port it. When Emacs 29 added Tree-sitter, I started a grammar for SCSS in April 2023, but it was hard going. I got stuck on a problem, wrote down a rough plan and put the project away. It stayed there for three and a half years.

At the end of September I had some unused credits for a coding agent, so I picked the project up again. This time I knew what I wanted:

  1. Complete highlighting. Every part of modern SCSS and CSS should get a color; syntax that only Internet Explorer understood can go.
  2. Structural editing from the start. I had tried it in JSX Jedi and liked the result, so the tree had to be shaped for editing commands, not just for colors.
  3. SCSS first. SCSS is a superset of CSS, so a grammar that handles SCSS handles CSS too. The older indented syntax (.sass) is not supported; SCSS has long been far more common.
  4. Narrow error recovery. Agents now write most of my code, and I mostly read it. Completion and agents produce well-formed code, so the parser only keeps the statement being typed from breaking the rest of the file and spends the rest of its effort on speed.

The result is tree-sitter-scss 1.0. On the 1,157 non-blank lines of my rhythm-sass library, the official grammar reports 700 error nodes; this one reports none. The comparison page runs both grammars on the same source, including any you paste in.

Two Emacs packages grew alongside the grammar: scss2-mode, which is new, and emmet2-mode, which I rewrote. Together they turn a few ideas I had years ago into tools I use every day.

scss2-mode

The package provides major modes for SCSS and CSS built on the grammar, and structural editing is the part I care about most. I described the idea in an earlier post, and JSX Jedi grew out of it. Each command looks at the syntax node under the cursor before deciding what to act on.

Take empty. It clears the contents of the structure the cursor is in, and what counts as contents depends on that structure:

  • On a declaration, margin: 0 auto; becomes margin: ;.
  • Inside a function call, calc(100% - 2rem) becomes calc().
  • On a selector, .card { color: red; } becomes .card {}.

Kill, copy, duplicate, comment and mark work the same way on the nearest declaration, map entry, rule or at-rule, and which node types they act on is a user option. Each edit is one undo step.

Two smaller features complete the package. Completion knows the scope: it offers the Sass variables, functions and mixins visible at the cursor, and the members of local modules loaded with @use. TAB moves through a declaration, from the property name to the value and then past the semicolon, so a block of declarations can be filled in with typing and TAB alone.

emmet2-mode

The first version of emmet2-mode ran the Emmet npm package in Deno through deno-bridge, with a layer of my own on top. It could expand an abbreviation from anywhere inside it, and it turned .card into className={css.card} for CSS Modules and m--gutter into margin: var(--gutter);. An uppercase letter started the value, as in mA for margin: auto;, because : was reserved for pseudo-classes. A comma joined declarations in place of +, which is easier to reach on a Dvorak keyboard. What it lacked was a preview: you pressed C-j and saw the result only afterwards.

Version 2.0 is written entirely in Emacs Lisp and runs no external process. CSS abbreviations are matched by their initials against a catalog of properties and values, so anyone used to Emmet can start right away: bgc gives background-color and tac gives text-align: center;. The matches appear as completion choices that depend on where the cursor is: declarations inside a rule, descriptors inside @font-face, values after a colon. Each choice previews its expansion, so I pick the right one before anything is inserted instead of fixing it afterwards.

Completion that depends on position is something I first did in the Sublime package. Its regular expressions assigned scopes to the code, and each completion list was limited to certain scopes, so property names appeared between declarations and values only after a colon. Writing highlighting with regular expressions, and completion on top of it, was tedious work, and it wore me out. This time an agent wrote most of the Emacs Lisp, and I got the behavior I wanted without learning much about package development. I doubt I would have had the patience to do all of this on my own.

Making it fast

With an agent doing most of the typing, every feature I had planned worked within a few days. The performance did not: the mode loaded no faster than the built-in scss-mode, highlighted large files more slowly, and took almost a minute to reindent a 724 KB file.

When I finally went through the code carefully, most of the cost came from things that did not need to be there. I had not reviewed closely enough during those days, and the agent often reached a goal by a route that worked but was not the best one. The mode still ran on top of css-mode, borrowing its indentation along with much else. Half of the nodes the parser built were hidden wrappers with a single child, so one color: red; allocated eight nodes of which only three carried information. The highlight query took 52 ms to compile for each dialect.

I removed these one at a time and measured each step against the same inputs. On my M1 Pro, scss2-mode now loads in about 25 ms against about 145 ms for scss-mode, reindents that 724 KB file in 0.7 seconds, and keeps its syntax trees at least a quarter smaller than before. On the files I actually edit, which are a few kilobytes each, editing feels no different from scss-mode; very large files still cost more because the whole file is parsed.

Most people now have agents write their stylesheets, so tools for editing them by hand may not find many users. I am still glad I built them.