Creating Custom Emmet Snippets In VS Code

Creating Custom Emmet Snippets In VS Code

Earlier this year, I shared the HTML boilerplate I like to use when starting new web projects with line-by-line explanations on my blog. It’s a collection of mostly <head> tags and attributes I usually use on every website I build. Until recently, I would just copy and paste the boilerplate whenever I needed it, but I’ve decided to improve my workflow by adding it as a snippet to VS Code — the editor of my choice.

  • Click “Add Item”, enter the path to the folder where you’ve saved the snippets.json file you’ve created earlier and press “OK”.
  • That’s it. Now we’re ready to create snippets by adding properties to the html and css objects where the key is the name of the snippet and the value an abbreviation or a string.

    Some Of My Custom HTML Snippets

    Before we dive deep into snippet creation and I show you how I created a snippet for my HTML boilerplate, let’s warm up first with some small, but useful snippets I’ve created, as well.

    Lazy Loading

    Out of the box, there’s an img abbreviation, but there’s none for lazily loaded images. We can use the default abbreviation and just add the additional attributes and attribute values we need in square brackets.

    {
      "html": {
        "snippets": {
          "img:l": "img[width height loading='lazy']"
        }
      }
    }
    

    img:l + Enter/Tab now creates the following markup:

    <img src="" alt="" width="" height="" loading="lazy">
    

    Page

    Most pages I create consist of <header>, <main> and <footer> landmarks and an <h1>. The custom page abbreviation lets me create that structure quickly.

    "snippets": {
      "page": "header>h1^main+footer{${0:©}}"
    }
    

    page + Enter/Tab creates the following markup:

    <header>
      <h1></h1>
    </header>
    <main></main>
    <footer>©</footer>
    

    That abbreviation is quite long, so let’s break it down into smaller bits.

    Breakdown

    Create an <header> element and a child <h1>.

    header>h1
    

    Move up, back to the level of the <header>, and create a <footer> that follows <main>.

    ^main+footer
    

    Set the final tab stop within the <footer> and set the default text to &copy.

    {${0:©}}
    

    Navigation

    The abbreviation nav just creates a <nav> start and end tag by default, but what I usually need is a <nav> with a nested <ul>, <li> elements and links ( <a>). If there are multiple <nav> elements on a page, they should also be labeled, for example by using aria-label.

    "nav": "nav[aria-label='${1:Main}']>ul>(li>a[aria-current='page']{${2:Current Page}})+(li*3>a{${0:Another Page}})"
    

    That looks wild, so let’s break it down again.

    Breakdown

    We start with a <nav> element with an aria-label attribute and a nested <ul>. ${1:Main} populates the attribute with the text “Main” and creates a tab stop at the attribute value by moving the cursor to it and highlighting it upon creation.

    nav[aria-label='${1:Main}']>ul
    

    Then we create four list items with nested links. The first item is special because it marks the active page using aria-current="page". We create another tab stop and populate the link with the text “Current Page”.

    (li>a[aria-current='page']>{${2:Current Page}})
    

    Finally, we add three more list items with links and the link text “Another page”.

    (li*3>a>{${0:Another Page}})
    

    Before our adaptations, we got this:

    Now we get this:

    <-- After: nav + TAB/Enter -->
    
    <nav aria-label="Main">
      <ul>
        <li><a href="" aria-current="page">Current Page</a></li>
        <li><a href="">Another Page</a></li>
        <li><a href="">Another Page</a></li>
        <li><a href="">Another Page</a></li>
      </ul>
    </nav>
    

    Style

    The default style abbreviation only creates the <style> start and end tag, but usually when I use the <style> element I do it because I quickly want to test or debug something.

    Let’s add some default rules to the <style> tag:

    "style": "style>{\\* { box-sizing: border-box; \\}}+{
    ${1:*}:focus \\{${2: outline: 2px solid red; }\\} }+{
    ${0}}"
    Breakdown

    Some characters (e.g. $, *, { or }) have to be escaped using \\.

    style>{\\* { box-sizing: border-box; \\}}
    


    creates a linebreak and ${1:*} places the first tab stop at the selector *.

    {
    ${1:*}:focus \\{${2: outline: 2px solid red; }\\}}
    • Before: <style><style>
    • After:
      <style>
      * { box-sizing: border-box; }  
      *:focus { outline: 2px solid red; }
      </style>
      

    Alright, enough warming-up. Let’s create complex snippets. At first, I wanted to create a single snippet for my boilerplate, but I created three abbreviations that serve different needs.

    1. Small
    2. Medium
    3. Full

    Boilerplate Small

    This is a boilerplate for quick demos, it creates the following:

    • Basic site structure,
    • viewport meta tag,
    • Page title,
    • <style> element,
    • A <h1>.
    {
      "!": "{<!DOCTYPE html>}+html[lang=${1}${lang}]>(head>meta:utf+meta:vp+{}+title{${2:New document}}+{}+style)+body>(h1>{${3: New Document}})+{${0}}"
    }
    
    Breakdown

    A string with the doctype:

    {<!DOCTYPE html>}
    

    The <html> element with a lang attribute. The value of the lang attribute is a variable you can change in the VS code settings (Code → Preferences → Settings).

    html[lang=${1}${lang}]
    

    You can change the default natural language of the page by searching for “emmet variables” in VS Code settings and changing the lang variable. You can add your custom variables here, too.

    The <head> includes the charset meta tag, viewport meta tag, <title>, and <style> tag. {} creates a new line.

    (head>meta:utf+meta:vp+{}+title{${2:New document}}+{}+style)
    

    Let’s have a first quick look at what this gives us.

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
      <title>New document</title>
    </head>
    </html>
    

    Looks okay, but the meta:utf abbreviation creates the old way in HTML to define the charset and meta:vp creates two tab stops I don’t need because I never use a different setting for the viewport.

    Let’s overwrite these snippets before we move on.

    {
      "meta:vp": "meta[name=viewport content='width=device-width, initial-scale=1']",
      "meta:utf": "meta[charset=${charset}]"
    }
    

    Last but not least, the <body> element, an <h1> with default text, followed by the final tab stop.

    body>(h1>{${3: New Document}})+{${0}}
    

    The final boilerplate:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
    
      <title>New document</title>
    
      <style>
        * { box-sizing: border-box; }
    
        *:focus { outline: 2px solid red; } 
    
    
      </style>
    </head>
    <body>
      <h1> New Document</h1>
    
    </body>
    </html>
    

    For me, that’s the perfect minimal debugging setup.

    Boilerplate Medium

    While I use the first boilerplate only for quick demos, the second boilerplate can be used for complex pages. The snippet creates the following:

    • Basic site structure,
    • viewport meta tag,
    • Page title,
    • .no-js/.js classes,
    • External screen and print stylesheets,
    • description and theme-color meta tag,
    • Page structure.
    {
      "!!": "{<!DOCTYPE html>}+html[lang=${1}${lang}].no-js>{<!-- TODO: Check lang attribute --> }+(head>meta:utf+meta:vp+{}+title{${1:🛑 Change me}}+{}+(script[type=\"module\"]>{document.documentElement.classList.replace('no-js', 'js');})+{}+link:css+link:print+{}+meta[name=\"description\"][content=\"${2:🛑 Change me (up to ~155 characters)}\"]+{<!-- TODO: Change page description --> }+meta[name=\"theme-color\"][content=\"${2:#FF00FF}\"])+body>page"
    }
    

    Yeaaah, I know, that looks like gibberish. Let’s dissect it.

    Breakdown

    The doctype and the root element are like in the first example, but with an additional no-js class and a comment that reminds me to change the lang attribute, if necessary.

    {<!DOCTYPE html>}+html[lang=${1}${lang}].no-js>{ }
    

    The TODO Highlight extension makes the comment really pop.

    The <head> includes the charset meta tag, viewport meta tag, <title>. {} creates a new line.

    (head>meta:utf+meta:vp+{}+title{${1:🛑 Change me}}+{}
    

    A script with a line of JavaScript. I’m cutting the mustard at the JS module support. If a browser supports JavaScript modules, it means that it’s a browser that supports modern JavaScript (e.g. modules, ES 6 syntax, fetch, and so on). I ship most JS only to these browsers, and I use the js class in CSS, if the styling of a component is different, when JavaScript is active.

    (script[type=\"module\"]>{document.documentElement.classList.replace('no-js', 'js');})+{}
    

    Two <link> elements; the first links to the main stylesheet and the second to a print stylesheet.

    link:css+link:print+{}
    

    The page description:

    meta[name=\"description\"\][content=\"${2:🛑 Change me (up to ~155 characters)}\"]+{ }
    

    The theme-color meta tag:

    meta[name=\"theme-color\"\][content=\"${2:#FF00FF}\"])
    

    The body element and the basic page structure:

    body>page
    

    The final boilerplate looks like this:

    <!DOCTYPE html>
    <html lang="en" class="no-js">
    <!-- TODO: Check lang attribute --> 
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
      <title>🛑 Change me</title>
    
      <script type="module">
        document.documentElement.classList.replace('no-js', 'js');
      </script>
    
      <link rel="stylesheet" href="style.css">
      <link rel="stylesheet" href="print.css" media="print">
    
      <meta name="description" content="🛑 Change me (up to ~155 characters)">
      <!-- TODO: Change page description --> 
      <meta name="theme-color" content="#FF00FF">
    </head>
    <body>
      <header>
        <h1></h1>
      </header>
      <main></main>
      <footer>©</footer>
    </body>
    </html>
    

    Full Boilerplate

    The full boilerplate is similar to the second boilerplate; the differences are additional meta tags and a script tag.

    The snippet creates the following:

    • Basic site structure,
    • viewport meta tag,
    • Page title,
    • js/no-js classes,
    • External screen and print stylesheets,
    • description and open graph meta tags,
    • theme-color meta tag,
    • canonical <link> tag,
    • Favicon tags,
    • Page structure,
    • <script> tag.
    {
      "!!!": "{<!DOCTYPE html>}+html[lang=${1}${lang}].no-js>{<!-- TODO: Check lang attribute --> }+(head>meta:utf+meta:vp+{}+title{${1:🛑 Change me}}+{}+(script[type=\"module\"]>{document.documentElement.classList.replace('no-js', 'js');})+{}+link:css+link:print+{}+meta[property=\"og:title\"][content=\"${1:🛑 Change me}\"]+meta[name=\"description\"][content=\"${2:🛑 Change me (up to ~155 characters)}\"]+meta[property=\"og:description\"][content=\"${2:🛑 Change me (up to ~155 characters)}\"]+meta[property=\"og:image\"][content=\"${1:https://}\"]+meta[property=\"og:locale\"][content=\"${1:en_GB}\"]+meta[property=\"og:type\"][content=\"${1:website}\"]+meta[name=\"twitter:card\"][content=\"${1:summary_large_image}\"]+meta[property=\"og:url\"][content=\"${1:https://}\"]+{<!-- TODO: Change social media stuff --> }+{}+link[rel=\"canonical\"][href=\"${1:https://}\"]+{<!-- TODO: Change canonical link --> }+{}+link[rel=\"icon\"][href=\"${1:/favicon.ico}\"]+link[rel=\"icon\"][href=\"${1:/favicon.svg}\"][type=\"image/svg+xml\"]+link[rel=\"apple-touch-icon\"][href=\"${1:/apple-touch-icon.png}\"]+link[rel=\"manifest\"][href=\"${1:/my.webmanifest}\"]+{}+meta[name=\"theme-color\"][content=\"${2:#FF00FF}\"])+body>page+{}+script:src[type=\"module\"]"
    }
    

    This incredibly long snippet creates this:

    <!DOCTYPE html>
    <html lang="en" class="no-js">
    <!-- TODO: Check lang attribute --> 
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
      <title>🛑 Change me</title>
    
      <script type="module">
        document.documentElement.classList.replace('no-js', 'js');
      </script>
    
      <link rel="stylesheet" href="style.css">
      <link rel="stylesheet" href="print.css" media="print">
    
      <meta property="og:title" content="🛑 Change me">
      <meta name="description" content="🛑 Change me (up to ~155 characters)">
      <meta property="og:description" content="🛑 Change me (up to ~155 characters)">
      <meta property="og:image" content="https://">
      <meta property="og:locale" content="en_GB">
      <meta property="og:type" content="website">
      <meta name="twitter:card" content="summary_large_image">
      <meta property="og:url" content="https://">
      <!-- TODO: Change social media stuff --> 
    
      <link rel="canonical" href="https://">
      <!-- TODO: Change canonical link --> 
    
      <link rel="icon" href="/favicon.ico">
      <link rel="icon" href="/favicon.svg" type="image/svg+xml">
      <link rel="apple-touch-icon" href="/apple-touch-icon.png">
      <link rel="manifest" href="/my.webmanifest">
    
      <meta name="theme-color" content="#FF00FF">
    </head>
    <body>
      <header>
        <h1></h1>
      </header>
      <main></main>
      <footer>©</footer>
    
      <script src="" type="module"></script>
    </body>
    </html>
    

    Custom CSS Snippets

    For the sake of completeness, here are some of the CSS snippets I’m using.

    Debugging

    This snippet creates a 5px red outline with a custom offset.

    "debug": "outline: 5px solid red;
    outline-offset: -5px;"

    Centering

    A snippet that sets display to flex, and centers its child items.

    "center": "display: flex;
    justify-content: center;
    align-items: center;"

    Sticky

    Sets the position property to sticky, with two tab stops at the top and left property.

    "sticky": "position: sticky;
    top: ${1:0};
    left: ${2:0};"

    User Snippets

    At the beginning of this article, I mentioned that VS Code also provides custom snippets. The difference to Emmet snippets is that you can’t use abbreviations, but you can also define tab stops and make use of internal variables.

    How to get the best out of user snippets could be a topic for another article, but here’s an example of a custom CSS snippet I’ve defined:

    "Visually hidden": {
    "prefix": "vh",
    "body": [
      ".u-vh {",
      "  position: absolute;
    white-space: nowrap;
    width: 1px;
    height: 1px;
    overflow: hidden;
    border: 0;
    padding: 0;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    margin: -1px;", "}" ], "description": "A utility class for screen reader accessible hiding." }

    This snippet doesn’t just create CSS rules, but a whole declaration block when we type vh and press Enter or Tab.

    .u-vh {
      position: absolute;
      white-space: nowrap;
      width: 1px;
      height: 1px;
      overflow: hidden;
      border: 0;
      padding: 0;
      clip: rect(0 0 0 0);
      clip-path: inset(50%);
      margin: -1px;
    }
    

    Final Words

    It takes some time to create these snippets, but it’s worth the effort because you can customize Emmet to your personal preferences, automate repetitive tasks and save time in the long run.

    I’d love to see which snippets you use, so please share them with us in the comments. If you want to use my settings, you can find my final snippets.json on GitHub.

    Resources

    • Default CSS Emmet snippets
    • Default HTML Emmet snippets
    • Emmet cheat sheet
    • Emmet in VS Code docs