Tutorial

Your First Custom Element

Published on September 5, 2017
    Default avatar

    By Alligator.io

    Your First Custom Element

    While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

    So your interested in learning about Web Components and creating your own custom HTML tags? In this post we’ll explore the basic syntax and concepts to allow you to start dabbling with Custom Elements and Shadow DOM.

    We’ll create a silly <my-title> custom element that simply stamps out a styled title. Not very useful, but it’ll help demonstrate a few starting concepts.

    Getting Started

    First we’ll create a separate JavaScript file that will contain everything about our custom element: its style rules, its markup, the ES6 class definition and finally register the custom element. In HTML files where we want to use our custom element, all we’ll have to do is to include that JavaScript file and we’ll be good to go to start using the new tag on our page.

    Let’s wrap everything in an IIFE for good measure:

    my-title.js
    (function() {
      // the good stuff goes here
    })();
    

    Now let’s define a class for our custom element, which should extend HTMLElement:

    (function() {
      class MyTitle extends HTMLElement {
        connectedCallback() {
          this.innerHTML = `
            <style>
              h1 {
                font-size: 2.5rem;
                color: hotpink;
                font-family: monospace;
                text-align: center;
                text-decoration: pink solid underline;
                text-decoration-skip: ink;
              }
            </style>
            <h1>Hello Alligator!</h1>
          `;
        }
      }
    
      window.customElements.define('my-title', MyTitle);
    })();
    

    Here are a few things to note:

    • Inside the ES6 class, this refers to the element instance itself.
    • The connectedCallback method is triggered once the element is added to the DOM. There’s also a disconnectedCallback method that gets called upon removing an element from the DOM that’s useful for cleaning-up things like event handlers.
    • We register (define) the custom element with customElement.define and passing-in the tag name as the first argument and then the class that defines the element as the second argument. Defining the element is what then allows us to use it in our HTML documents. Note that tag names should be at least two words, separated with an hyphen. That’s to prevent any future HTML element from colliding with custom elements.

    With such a simple custom element, you could also define the class as an anonymous class directly in the call to customElements.define:

    (function() {
      window.customElements.define(
        'my-title',
        class extends HTMLElement {
          connectedCallback() {
            this.innerHTML = `
              <style>
                h1 {
                  font-size: 2.5rem;
                  color: hotpink;
                  font-family: monospace;
                  text-align: center;
                  text-decoration: pink solid underline;
                  text-decoration-skip: ink;
                }
              </style>
              <h1>Hello Alligator!</h1>
            `;
          }
        }
      );
    })();
    

    You’ll notice that everything is contained within the JavaScript and that we don’t have any standalone HTML markup. That’s because, perhaps unfortunately, HTML imports seem to be dead in the water, and the way forward for Web Components will be to define markup and styles in the JavaScript using ES6 string literals.

    Shadow DOM

    The above example is all well and good, but there’s one major problem: our styles are not scoped to our custom element. That means that now all h1 tags on our pages will be hotpink with an underline. For this component’s styles not to impact its outer world, a ad hoc solution would be to wrap our markup inside something like a div and then apply styles with a selector that targets only our wrapper:

    this.innerHTML = `
      <style>
        .wrap-my-title h1 {
          font-size: 2.5rem;
          color: hotpink;
          font-family: monospace;
          text-align: center;
          text-decoration: pink solid underline;
          text-decoration-skip: ink;
        }
      </style>
      <div class="wrap-my-tile">
        <h1>Hello Alligator!</h1>
      </div>
    `;
    

    That’s not great and is not even foolproof if there’s another element with the wrap-my-title somewhere in your markup. Plus, it would be more performant and much nicer if we could use simple CSS selectors like h1. This is where Shadow DOM comes in. Shadow DOM allows us to scope our styles to our custom elements so that they don’t bleed out.

    To use Shadow DOM, you attach a shadow root to the element and then define the markup for the element inside the shadow root:

    (function() {
      class MyTitle extends HTMLElement {
        constructor() {
          super();
    
          this.attachShadow({ mode: 'open' });
          this.shadowRoot.innerHTML = `
            <style>
              h1 {
                font-size: 2.5rem;
                color: hotpink;
                font-family: monospace;
                text-align: center;
                text-decoration: pink solid underline;
                text-decoration-skip: ink;
              }
            </style>
            <h1>Hello Alligator!</h1>
          `;
        }
      }
    
      window.customElements.define('my-title', MyTitle);
    })();
    

    Here are a few things to note:

    • The class constructor is a good place to attach a shadow root and define its inner html.
    • When a custom element’s class has a constructor, you should always call super() in it.
    • The mode for the shadow root can be either open or closed. You’ll probably only ever use open, because otherwise you wouldn’t be able to set any innerHTML for it.

    Using Shadow DOM, here’s what the markup will look like in your browser’s console:

    view of Shadow DOM in the console

    Alternate syntax

    You could also accomplish the same result by creating a template element, setting its innerHTML and then cloning the content of the template as a new child to our shadow root:

    (function() {
      const template = document.createElement('template');
    
      template.innerHTML = `
          <style>
            h1 {
              font-size: 2.5rem;
              color: hotpink;
              font-family: monospace;
              text-align: center;
              text-decoration: pink solid underline;
              text-decoration-skip: ink;
            }
          </style>
          <h1>Hello Alligator!</h1>
      `;
    
      class MyTitle extends HTMLElement {
        constructor() {
          super();
    
          this.attachShadow({ mode: 'open' });
          this.shadowRoot.appendChild(template.content.cloneNode(true));
        }
      }
    
      window.customElements.define('my-title', MyTitle);
    })();
    

    Usage

    Using our custom element is as simple as adding the script file to the page and then using our element as we would any other regular HTML element. Note however that custom elements should always have a closing tag:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <script src="./my-counter.js"></script>
    </head>
    
      <body>
        <my-title></my-title>
      </body>
    
    </html>
    

    Keep in mind that our element is not quite production ready. As it is now, the element will only work in a few modern browsers. You’ll want to run the code through a transpiler like Babel for JavaScript features that are not supported across the board like ES6 classes or string literals and you’ll want to use polyfills for custom elements and shadow DOM. We’ll go over using Web Components polyfills in a separate post.

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about us


    About the authors
    Default avatar
    Alligator.io

    author

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Get our biweekly newsletter

    Sign up for Infrastructure as a Newsletter.

    Hollie's Hub for Good

    Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

    Become a contributor

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    Welcome to the developer cloud

    DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

    Learn more
    DigitalOcean Cloud Control Panel