Emmet Enhancements for Web Development

Jul 3, 2023[ Emmet , Emacs , Deno ]

As a front-end developer, Emmet is a tool I rely on daily. I first started using it over a decade ago, back when it was still known as “Zen Coding”. This highly efficient tool has been a major timesaver and has significantly enhanced my work experience. Over the years, I’ve had sporadic ideas on how to improve it, for example, expand m--gutter to margin: var(--gutter);. These insights would flash into my mind and I’d jot them down, eventually leading to a strong desire to customize Emmet to my personal tastes.

My initial attempt at customization was the Sublime Text package Emmet Css Snippets that I created 11 years ago. This package is a compilation of 200-300 snippets that adhere to Emmet’s abbreviation rules, but with a few personal tweaks such as simplified abbreviations and added pseudo-class abbreviations. At the time, I didn’t know any scripting language (including JavaScript), so I manually created each snippet. It was a time-consuming process, but as a side benefit, I now have an excellent recall of CSS properties and values.

However, I only used Sublime Text for two years before switching to Emacs. I began using the emmet-mode and created my own fork (which has since been removed from GitHub). With my limited knowledge of Emacs Lisp, I was only able to modify the snippets in the same way I had done in the Emmet Css Snippets package, and couldn’t take it any further.

About nine months ago, one of my favorite Emacs developers, @manateelazycat, introduced the deno-bridge package. This development allowed us to use Deno as the back-end for Emacs plugins. The icing on the cake is that Deno can leverage all NPM packages. This breakthrough finally gave me the opportunity to realize my long-standing ambition. I can now have Emmet process my abbreviations in Emacs, intercept user input before it reaches Emmet, and also alter the content that Emmet returns.

So I created the emmet2-mode, an Emmet-enhanced minor mode for Emacs. I’ve been using it for over eight months now and it’s been performing as expected. It perfectly caters to my personal needs and saves me even more time than Emmet itself. Now, I’m excited to share how I made this possible.

Enhancing Markup

First, let’s take a look at the markup aspect. While Emmet’s markup abbreviations are expertly designed and I don’t intend to alter them, I have added three additional features:

Expanding Abbreviations from Any Position

I find it inconvenient that Emmet only extracts abbreviations backwards. This means that after moving the cursor back to make some changes, I also have to move it back to the end of the edited abbreviation to expand it. Ideally, I would want it to automatically detect the abbreviation, allowing me to expand it from any position. To achieve this, I didn’t use the extract function provided by Emmet. Instead, I created a new extraction function using regex to pinpoint all potential abbreviations in a line, and then selected the correct one based on the current cursor position. Now, when I’m working with the following abbreviation:

ul>li*5>h4{title placeholder}+p{content placeholder}

I can expand the abbreviation from any part of it, making content edits a breeze.

Support for CSS Modules in JSX

The CSS Modules is another essential tool that I use daily, probably in all my projects. Therefore, expanding .abc to <div className={css.abc}></div> is much more practical than expanding to <div class="abc"></div>. The css here is my CSS Modules object, which can be customized, as I’ll explain later.

At the time of writing this post, Emmet has added this feature in version 2.4.5. However, I would ideally like to expand .a.b.c to <div className={clsx(css.a, css.b, css.c)}></div>. The clsx() here is my class names constructor. Currently, my approach may not align with Emmet’s.

Thanks to Emacs Directory Variables, both the CSS Modules object and the class names constructor can be customized as project-based local variables. Simply create a .dir-locals.el file at your project’s root and add the following code:

((web-mode . ((emmet2-css-modules-object . "style")
              (emmet2-class-names-constructor . "classnames"))))

There is also a (emmet2-markup-variant . "solid") config to let emmet2-mode work with Solid.js.

CSS Expansion in Markup

Although I rarely use this feature, having the ability to expand CSS in markup is quite handy. This feature was inspired by emmet-mode.

When the cursor is inside the <style>|</style> tag or <div style="|"></div> attribute, it automatically expands CSS. And, when the cursor is inside the <div style={{|}} /> JSX attribute, it expands CSS in JS instead.

Enhancing CSS

Currently, I primarily use the SCSS syntax, which is a superset to CSS, much like TypeScript is to JavaScript. Hence, the CSS enhancements are essentially SCSS enhancements, and some of them are opinionated.

Shorthand Alias

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 force myself to add z-index to all absolute and fixed positions.

// posa ->
position: absolute;
z-index: |;

// posf1000 ->
position: fixed;
z-index: 1000;

I am planning to completely match CSS properties and values dynamically. Let’s see what I can come up with in the future.

Personal SCSS Functions

The Modular Scale ms() function and the rhythm() function are commonly used in all my projects.

// t(2) ->
top: rhythm(2);
// fz(1) ->
font-size: ms(1);

These two abbreviations can significantly enhance my productivity. I’ll introduce these in more detail in a future blog post.

Custom Properties

CSS custom properties are now widely supported, so it’s:

// m--gutter ->
margin: var(--gutter);
// p--a--b--c ->
padding: var(--a) var(--b) var(--c);

camelCase Alias

In Emmet, : and - are used to separate properties and values, for example, ma expand to max-width: ; but m:a expands to margin: auto;. I find : not as easy to type, and it’s better to use it as the trigger to expand pseudo-selectors. Therefore, I introduced a camelCase alias. Now, mA is equivalent to both m:a and m-a.

// ma ->
max-width: ;
// mA == m:a == m-a ->
margin: auto;

Comma as Abbreviation Splitter

As a Dvorak keyboard layout user, I find the comma much easier to type than + to join abbreviations.

t0,r0,b0,l0 == t0+r0+b0+l0

At Rules

I’m always looking for ways to be more efficient, even if it means typing fewer characters.

@cs -> @charset
@kf -> @keyframes
@md -> @media

@us -> @use "|";
@in -> @if not | {}

Expanding Pseudo-Classes and Pseudo-Elements

As mentioned earlier, : is now used as the trigger to expand pseudo-classes or pseudo-elements, starting with the : symbol, followed by two or three distinct letters.

// :be ->
&::before {

// .class:fu ->
.class::focus {

There’s also an easy way to expand functional pseudo-classes:

// :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 pseudo-selectors and CSS at-rules list are borrowed from the VS Code Custom Data.

Well, that wraps up all the improvements I’ve implemented for Emmet. For more details, please have a look at the emmet2-mode repo. I want to extend my gratitude to the dedicated Emmet team. I managed to accomplish this by building on the hard work they’ve done.

Did you enjoy reading this post? I'd be thrilled to have you join me on my journey of exploration and creation. Let's keep discussing this post on Twitter and collaborate on GitHub. Keep in touch!