Extending Emmet in Emacs
As a front-end developer, I rely on Emmet every day. I started using it over a decade ago, when it was still called “Zen Coding”. It has saved me a great deal of time and made my work much easier. Over the years, I had occasional ideas for improving it, such as expanding m--gutter to margin: var(--gutter);. I wrote each idea down when it came to me, and over time they grew into a strong wish to customize Emmet to my own taste.
My first attempt was Emmet Css Snippets, a Sublime Text package I created 11 years ago. It holds 200 to 300 snippets that follow Emmet’s abbreviation rules, with a few changes of my own, such as shorter abbreviations and new ones for pseudo-classes. I did not know any scripting language at the time, not even JavaScript, so I wrote every snippet by hand. It took a long time, but as a side benefit, I now remember CSS properties and values very well.
I used Sublime Text for only two years before switching to Emacs. In Emacs, I started using emmet-mode and made my own fork of it, which is no longer on GitHub. With the little Emacs Lisp I knew, I could only change the snippets, as I had done in Emmet Css Snippets, and could not go further.
About nine months ago, @manateelazycat, one of my favorite Emacs developers, introduced deno-bridge, which lets Emacs plugins use Deno as a back end. Better still, Deno can use any npm package. This was the chance I had been waiting for. Now I can have Emmet process my abbreviations in Emacs, intercept user input before it reaches Emmet and change the text Emmet returns.
So I created emmet2-mode, a minor mode for Emacs that enhances Emmet. I have used it for over eight months, and it works as I expected. It fits my needs exactly and saves me even more time than Emmet alone. Here is how I made it work.
Enhancing markup
I will start with markup. Emmet’s markup abbreviations are well designed, and I do not intend to change them, but I have added three features.
Expanding abbreviations from any position
I find it inconvenient that Emmet reads abbreviations only backward from the cursor. If I move the cursor back to edit an abbreviation, I then have to move the cursor to the end of the abbreviation again before expanding. I wanted the abbreviation detected automatically, so that I could expand it from any position. Instead of the extract function that Emmet provides, I wrote my own extraction function: a regular expression finds every possible abbreviation on the line, and the function picks the right one based on the cursor position. Take this abbreviation:
ul>li*5>h4{title placeholder}+p{content placeholder}I can expand this abbreviation with the cursor anywhere in it, which makes editing the content much easier.
Support for CSS Modules in JSX
I also use CSS Modules every day, probably in all my projects. For this reason, expanding .abc to <div className={css.abc}></div> is much more useful to me than expanding it to <div class="abc"></div>. Here css is the name of my CSS Modules object; the name is configurable, as shown below.
Emmet 2.4.0, released in March 2023, added a similar feature that marks a CSS Modules class with a double dot: ..abc. In emmet2-mode, several classes go through a class names constructor, clsx() by default: .a.b.c expands to <div className={clsx(css.a, css.b, css.c)}></div>.
Both the CSS Modules object and the class names constructor can be set per project with directory variables. Create a .dir-locals.el file at the project root and add:
((web-mode . ((emmet2-css-modules-object . "style")
(emmet2-class-names-constructor . "classnames"))))Setting (emmet2-markup-variant . "solid") also makes emmet2-mode work with Solid.js.
CSS expansion in markup
I rarely use this feature, but it is quite handy. It was inspired by emmet-mode.
Inside a <style>|</style> element or a <div style="|"></div> attribute, abbreviations expand as CSS. Inside a JSX <div style={{|}} /> attribute, they expand as CSS-in-JS instead.
Enhancing CSS
Currently, I mostly write SCSS. The CSS enhancements are therefore really SCSS enhancements, and some of them are opinionated.
Shorthand aliases
Numeric font-weight shorthands:
// fw2 ->
font-weight: 200;
// fw7 ->
font-weight: 700;Full width and full height shorthands:
// wf ->
width: 100%;
// hf ->
height: 100%;I make myself add a z-index whenever I use absolute or fixed positioning:
// posa ->
position: absolute;
z-index: |;
// posf1000 ->
position: fixed;
z-index: 1000;Other property and value abbreviations go through Emmet’s fixed snippet table. I plan to match them against CSS data instead, the way at-rules and pseudo-selectors are matched below.
Personal SCSS functions
I use two functions in all my projects: ms() from Modular Scale and rhythm() from rhythm-sass.
// t(2) ->
top: rhythm(2);
// fz(1) ->
font-size: ms(1);These two abbreviations save me a lot of time.
Custom properties
CSS custom properties are now widely supported, so emmet2-mode expands them too:
// m--gutter ->
margin: var(--gutter);
// p--a--b--c ->
padding: var(--a) var(--b) var(--c);camelCase alias
In Emmet, : or - separates a property from its value: ma expands to max-width: ;, but m:a expands to margin: auto;. I find : harder to type, and I would rather use it to expand pseudo-selectors. So I added a camelCase alias: mA is the same as both m:a and m-a.
// ma ->
max-width: ;
// mA == m:a == m-a ->
margin: auto;Comma between abbreviations
I use the Dvorak keyboard layout, where a comma is much easier to type than +. In emmet2-mode, a comma joins abbreviations as + does:
t0,r0,b0,l0 == t0+r0+b0+l0At-rules
I am always looking for ways to work faster, even when the saving is only a few characters:
@cs -> @charset
@kf -> @keyframes
@md -> @media
@us -> @use "|";
@in -> @if not | {}Expanding pseudo-classes and pseudo-elements
As mentioned above, : now triggers pseudo-selectors: type : followed by two or three letters that identify the pseudo-class or pseudo-element.
// :be ->
&::before {
|
}
// .class:fu ->
.class:focus {
|
}Functional pseudo-classes have short forms too:
// :n(:fc) ->
&:not(:first-child) {
|
}
// .class:n(:fc,:lc):be ->
.class:not(:first-child):not(:last-child)::before {
|
}
// :nc(2n-1) ->
&:nth-child(2n-1) {
|
}The lists of pseudo-selectors and CSS at-rules come from VS Code Custom Data.
That covers all the improvements I have made to Emmet. For more details, see the emmet2-mode repository. I want to thank the Emmet team: all of this builds on their hard work.