Artigo original: https://www.freecodecamp.org/news/reusable-html-components-how-to-reuse-a-header-and-footer-on-a-website/

Tradução realizada em português europeu

Imagina que estás a criar um site para um cliente, uma pequena loja familiar, que tem apenas duas páginas.

Não é muita coisa. Por isso, quando terminas a página principal e começas a trabalhar na página de contactos, simplesmente crias um ficheiro HTML e copias todo o código da primeira página.

O cabeçalho e o rodapé já têm bom aspeto e tudo o que precisas fazer é alterar o conteúdo restante.

O que fazer, no entanto, se o teu cliente quiser 10 páginas? Ou 20? Como farias se ele pedir pequenas alterações no cabeçalho e no rodapé ao longo do desenvolvimento?

De repente, qualquer alteração, por mais pequena que seja, tem de ser repetida ao longo de todos os ficheiros.

Esse é um dos maiores problemas que coisas como o React ou Handlebars.js ajudam a resolver: qualquer código, em especial, coisas estruturais como o cabeçalho ou o rodapé, pode ser escrito uma vez e reutilizado ao longo do projeto.

Até bem recentemente, era impossível utilizar componentes em HTML simples e em JavaScript. Porém, com a introdução dos Web Components, é possível criar componentes reutilizáveis sem utilizar coisas como o React.

O que são os Web Components?

Os Web Components são, na realidade, uma coleção de algumas tecnologias diferentes que te permitem criar elementos HTML personalizados.

Essas tecnologias são:

  • Templates HTML (texto em inglês): fragmentos de código HTML utilizando elementos <template> que não vão ser renderizados até que sejam anexados à página com JavaScript.
  • Elementos personalizados (texto em inglês): APIs do JavaScript com vasto suporte que te permitem criar elementos do DOM. Assim que criares e registares um elemento personalizado através dessas APIs, poderás utilizá-lo de modo semelhante a um componente do React.
  • Shadow DOM (texto em inglês): é um DOM menor, encapsulado, que está isolado do DOM principal e renderizado separadamente. Os estilos e scripts que crias para teus componentes personalizados no Shadow DOM não afetarão outros elementos no DOM principal.

Vamos abordar cada uma dessas tecnologias ao longo do tutorial.

Como utilizar templates de HTML

A primeira peça do quebra-cabeças é aprender como utilizar templates de HTML para criar código HTML reutilizável.

Vamos observar um exemplo de uma simples mensagem de boas-vindas:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script src="index.js" type="text/javascript" defer></script>
  </head>
  <body>
    <template id="welcome-msg">
      <h1>Hello, World!</h1>
      <p>And all who inhabit it</p>
    </template>
  </body>
<html>
index.html

Se observares a página, nem os elementos <h1> nem os <p> são renderizados. Se, contudo, abrires a consola de desenvolvimento, verás que ambos os elementos foram analisados:

image-50

Para renderizar mesmo a mensagem de boas-vindas, vais precisar de utilizar um pouco de JavaScript:

const template = document.getElementById('welcome-msg');

document.body.appendChild(template.content);
index.js
image-51

Embora esse seja um exemplo bastante simples, já é possível veres como a utilização de templates torna mais fácil reutilizar código ao longo da página.

O principal problema é que, pelo menos no exemplo atual, o código da mensagem de boas-vindas está misturado com o resto do conteúdo da página. Se desejares alterar a mensagem mais tarde, terás de alterar o código em vários ficheiros.

Em vez disso, podes puxar o template HTML para o ficheiro JavaScript, de maneira a que qualquer página onde o JavaScript está incluido irá renderizar a mensagem de boas-vindas:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script src="index.js" type="text/javascript" defer></script>
  </head>
  <body>
      
  </body>
<html>
index.html
const template = document.createElement('template');

template.innerHTML = `
  <h1>Hello, World!</h1>
  <p>And all who inhabit it</p>
`;

document.body.appendChild(template.content);
index.js

Agora que temos tudo no ficheiro JavaScript, não é necessário criares um elemento <template> – podes simplesmente criar um elemento <div> ou <span>.

No entanto, os elementos <template> podem ser combinados com um elemento <slot>, que te permite fazer coisas como alterar o texto para elementos dentro de <template>. É um pouco fora do âmbito deste tutorial. Por isso, podes ler mais sobre elementos <slot> na MDN (texto em inglês).

Como criar elementos personalizados

Uma coisa que podes ter reparado ao utilizar templates de HTML é que pode ser complicado inserir o teu código no local correto. O exemplo anterior da mensagem de boas-vindas estava apenas anexado à página.

Se já existisse conteúdo na página, digamos, uma imagem de banner, a mensagem de boas-vindas apareceria abaixo da imagem.

Como elemento personalizado, a tua mensagem de boas-vindas poderia ter este aspeto:

<welcome-message></welcome-message>

Poderias colocá-la em qualquer local da página.

Com isto em mente, vamos observar os elementos personalizados e criar os nossos próprios elementos de cabeçalho e rodapé ao estilo do React.

Configuração

Para um site de portfólio, podes ter algum código predefinido que se pareça com isto:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <main>
      <!-- O conteúdo da tua página -->
    </main>
  </body>
<html>
index.html
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  height: 100%;
}

body {
  color: #333;
  font-family: sans-serif;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1 0 auto;
}
style.css

Cada página terá o mesmo cabeçalho e rodapé. Por isso, faz sentido criar um elemento personalizado para cada um deles.

Vamos começar pelo cabeçalho.

Definir um elemento personalizado

Primeiro, cria uma pasta chamada components. Dentro dessa pasta, cria um ficheiro chamado header.js, com o seguinte código:

class Header extends HTMLElement {
  constructor() {
    super();
  }
}
components/header.js

É apenas uma simples class do ES5 a declarar o teu componente Header personalizado, com o método constructor e a palavra-chave especial super. Podes ler mais sobre isto na MDN.

Ao estender a classe genérica HTMLElement, podes criar qualquer tipo de elemento que queiras. Também é possível extender elementos específicos como HTMLParagraphElement.

Registar o teu elemento personalizado

Antes de começares a utilizar o teu elemento personalizado, vais precisar de o registar com o método customElements.define():

class Header extends HTMLElement {
  constructor() {
    super();
  }
}

customElements.define('header-component', Header);
components/header.js

Esse método recebe pelo menos dois argumentos.

O primeiro é uma DOMString que vais utilizar quando estiveres a adicionar o componente à página, neste caso, <header-component></header-component>.

O próximo é a classe do componente que criaste anteriormente, neste caso, a classe Header.

O terceiro argumento, é opcional, e descreve qual é o elemento HTML existente do qual o teu elemento personalizado optional vai herdar propriedades, por exemplo, {extends: 'p'}. No entanto, não vamos utilizar esta funcionalidade neste tutorial.

Utilizar callbacks de ciclo de vida para adicionar o cabeçalho à página

Existem quatro callbacks de ciclo de vida especiais para elementos personalizados que podemos utilizar para anexar o código do cabeçalho à página: connectedCallback, attributeChangeCallback, disconnectedCallback e adoptedCallback.

Destas callbacks, connectedCallback é uma das mais utilizadas. connectedCallback executa cada vez que o teu elemento personalizado é inserido no DOM.

Podes ler mais sobre outras callbacks aqui.

Para o nosso simples exemplo, connectedCallback é suficiente para adicionar um cabeçalho à página:

class Header extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this.innerHTML = `
      <style>
        nav {
          height: 40px;
          display: flex;
          align-items: center;
          justify-content: center;
          background-color:  #0a0a23;
        }

        ul {
          padding: 0;
        }
        
        a {
          font-weight: 700;
          margin: 0 25px;
          color: #fff;
          text-decoration: none;
        }
        
        a:hover {
          padding-bottom: 5px;
          box-shadow: inset 0 -2px 0 0 #fff;
        }
      </style>
      <header>
        <nav>
          <ul>
            <li><a href="about.html">About</a></li>
            <li><a href="work.html">Work</a></li>
            <li><a href="contact.html">Contact</a></li>
          </ul>
        </nav>
      </header>
    `;
  }
}

customElements.define('header-component', Header);
components/header.js

Depois, no index.html, adiciona o script components/header.js e <header-component></header-component> logo acima do elemento <main>:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script src="components/header.js" type="text/javascript" defer></script>
  </head>
  <body>
    <header-component></header-component>
    <main>
      <!-- O conteúdo da tua página -->
    </main>
  </body>
<html>
index.html

O teu componente de cabeçalho reutilizável, agora, deve ser renderizado na página:

image-54

Agora, adicionar um cabeçalho à página é tão simples como adicionar uma tag <script> a apontar para components/header.js, e adicionar <header-component></header-component> onde quiseres.

Nota que, visto que o cabeçalho e a sua estilização estão a ser inseridos diretamente no DOM principal, é possível estilizá-lo no ficheiro style.css.

Se, contudo, observares os estilos do cabeçalho em connectedCallback, estes são bastante genéricos e podem afetar outros estilos na página.

Por exemplo, se adicionarmos o Font Awesome e um componente de rodapé a index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA==" crossorigin="anonymous" />
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script src="components/header.js" type="text/javascript" defer></script>
    <script src="components/footer.js" type="text/javascript" defer></script>
  </head>
  <body>
    <header-component></header-component>
    <main>
      <!-- O conteúdo da tua página -->
    </main>
    <footer-component></footer-component>
  </body>
<html>
index.html
class Footer extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this.innerHTML = `
      <style>
        footer {
          height: 60px;
          padding: 0 10px;
          list-style: none;
          display: flex;
          justify-content: space-between;
          align-items: center;
          background-color: #dfdfe2;
        }
        
        ul li {
          list-style: none;
          display: inline;
        }
        
        a {
          margin: 0 15px;
          color: inherit;
          text-decoration: none;
        }
        
        a:hover {
          padding-bottom: 5px;
          box-shadow: inset 0 -2px 0 0 #333;
        }
        
        .social-row {
          font-size: 20px;
        }
        
        .social-row li a {
          margin: 0 15px;
        }
      </style>
      <footer>
        <ul>
          <li><a href="about.html">About</a></li>
          <li><a href="work.html">Work</a></li>
          <li><a href="contact.html">Contact</a></li>
        </ul>
        <ul class="social-row">
          <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
          <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
          <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
        </ul>
      </footer>
    `;
  }
}

customElements.define('footer-component', Footer);
components/footer.js

Aqui está como ficaria o aspeto da página:

image-55

A estilização do rodapé sobrepõe-se à estilização do cabeçalho, alterando a cor dos links. Este é o comportamento esperado para o CSS, mas seria interessante se a estilização de cada componente estivesse vinculada a esse componente, sem afetar outras coisas na página.

Bem, é mesmo onde o Shadow DOM brilha. Ou escurece? De qualquer modo, o Shadow DOM pode fazer isso.

Como utilizar o Shadow DOM com elementos personalizados

O Shadow DOM funciona como uma instância separada e mais pequena do DOM principal. Em vez de funcionar como uma cópia do DOM principal, o Shadow DOM é mais uma sub-árvore apenas para o teu elemento personalizado. Qualquer coisa adicionada a ele, especialmente os estilos, está vinculada a esse elemento personalizado em particular.

De certo modo, é como utilizar const e let em vez de var.

Vamos começar por refatorizar o componente de cabeçalho:

const headerTemplate = document.createElement('template');

headerTemplate.innerHTML = `
  <style>
    nav {
      height: 40px;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color:  #0a0a23;
    }

    ul {
      padding: 0;
    }
    
    ul li {
      list-style: none;
      display: inline;
    }
    
    a {
      font-weight: 700;
      margin: 0 25px;
      color: #fff;
      text-decoration: none;
    }
    
    a:hover {
      padding-bottom: 5px;
      box-shadow: inset 0 -2px 0 0 #fff;
    }
  </style>
  <header>
    <nav>
      <ul>
        <li><a href="about.html">About</a></li>
        <li><a href="work.html">Work</a></li>
        <li><a href="contact.html">Contact</a></li>
      </ul>
    </nav>
  </header>
`;

class Header extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    
  }
}

customElements.define('header-component', Header);
components/header.js

A primeira coisa que precisas fazer é utilizar o método .attachShadow() para anexar uma raiz do Shadow Dom ao teu elemento de componente de cabeçalho personalizado. Em connectedCallback, adiciona o seguinte código:

...
class Header extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'closed' });
  }
}

customElements.define('header-component', Header);
components/header.js

Repara que estamos a passar um objeto para .attachShadow() com uma opção, mode: 'closed'. Isto significa apenas que o Shadow DOM do componente de cabeçalho não é acessível a partir de JavaScript externo.

Se quiseres manipular o Shadow DOM do componente de cabeçalho mais tarde com JavaScript fora do ficheiro components/header.js, simplesmente muda a opção para mode: 'open'.

Por fim, anexa shadowRoot à página com o método .appendChild():

...

class Header extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'closed' });

    shadowRoot.appendChild(headerTemplate.content);
  }
}

customElements.define('header-component', Header);
components/header.js

Agora, visto que os estilos de componente do cabeçalho estão encapsulados no seu Shadow DOM, a página deve ter este aspeto:

image-56

Aqui está o componente de rodapé refatorizado para utilizar o Shadow DOM:

const footerTemplate = document.createElement('template');

footerTemplate.innerHTML = `
  <style>
    footer {
      height: 60px;
      padding: 0 10px;
      list-style: none;
      display: flex;
      flex-shrink: 0;
      justify-content: space-between;
      align-items: center;
      background-color: #dfdfe2;
    }

    ul {
      padding: 0;
    }
    
    ul li {
      list-style: none;
      display: inline;
    }
    
    a {
      margin: 0 15px;
      color: inherit;
      text-decoration: none;
    }
    
    a:hover {
      padding-bottom: 5px;
      box-shadow: inset 0 -2px 0 0 #333;
    }
    
    .social-row {
      font-size: 20px;
    }
    
    .social-row li a {
      margin: 0 15px;
    }
  </style>
  <footer>
    <ul>
      <li><a href="about.html">About</a></li>
      <li><a href="work.html">Work</a></li>
      <li><a href="contact.html">Contact</a></li>
    </ul>
    <ul class="social-row">
      <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
      <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
      <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
    </ul>
  </footer>
`;

class Footer extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'closed' });

    shadowRoot.appendChild(footerTemplate.content);
  }
}

customElements.define('footer-component', Footer);
components/footer.js

Contudo, se observares a página, podes ver que os ícones do Font Awesome estão em falta:

missing-fa-icons-1

Agora que o componente de rodapé está encapsulado dentro do seu próprio Shadow DOM, já não tem acesso ao link CDN do Font Awesome em index.html.

Vamos observar rapidamente a razão disto acontecer e como voltar a colocar o Font Awesome a funcionar.

Encapsulamento e o Shadow DOM

Enquanto que o Shadow DOM impede que os estilos dos teus componentes afetem o resto da página, alguns estilos globais podem infiltrar-se na mesma nos teus componentes.

Nos exemplos acima, isto era uma funcionalidade útil. Por exemplo, o componente de rodapé herda a declaração color: #333 que é definida em style.css. Isto acontece porque color é uma das propriedades que podem ser herdadas, juntamente com font, font-family, direction, entre outras.

Se quiseres impedir este comportamento e estilizar cada componente completamente do zero, podes fazê-lo com apenas algumas linhas de CSS:

:host {
  all: initial;
  display: block;
}

:host é um pseudosseletor que seleciona o elemento que está a hospedar o Shadow DOM. Neste caso, é o teu componente personalizado.

Então, a declaração all: initial redefine todas as propriedades CSS de volta para os seus valores iniciais. display: block faz a mesma coisa para a propriedade display e redefine a propriedade de volta para a predefinição do navegador, block.

Para uma lista completa de propriedades do CSS que podem ser herdadas, observa esta resposta no Stack Overflow (texto em inglês).

Como utilizar o Font Awesome com o Shadow DOM

Agora, podes estar a pensar, se font, font-family e outras propriedades do CSS relacionadas com tipos de letra são propriedades que podem ser herdadas, porque é que o Font Awesome não carrega agora que o componente de rodapé está a utilizar o Shadow DOM?

Acontece que, para coisas como fontes e outros recursos, estas precisam de ser referenciadas tanto no DOM principal como no Shadow DOM para funcionar corretamente.

Felizmente, existem algumas formas simples de corrigir isto.

Observação: todos estes métodos ainda requerem que o Font Awesome seja incluído em index.html, com o elemento link tal como no trecho de código acima.

A forma mais direta de fazer com que o Font Awesome funcione no teu componente do Shadow DOM é incluir um link para ele dentro do próprio componente:

const footerTemplate = document.createElement('template');

footerTemplate.innerHTML = `
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA==" crossorigin="anonymous" />
  <style>
    footer {
      height: 60px;
      padding: 0 10px;
      list-style: none;
...
components/footer.js

Uma coisa a ter em conta é que, embora pareça que estás a causar que o navegador carregue duas vezes o Font Awesome (uma para o DOM principal e novamente para o componente), os navegadores são suficientemente inteligentes para não buscar o mesmo recurso novamente.

Aqui está o separador de rede a mostrar que o Chrome vai buscar o Font Awesome uma só vez:

Screenshot-from-2021-09-12-14-53-01

Nº 2: Importar o Font Awesome dentro do teu componente

De seguida, podes utilizar @import e url() para carregar o Font Awesome no teu componente:

const footerTemplate = document.createElement('template');

footerTemplate.innerHTML = `
  <style>
    @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css");

    footer {
      height: 60px;
      padding: 0 10px;
      list-style: none;
...

Nota que o URL deve ser o mesmo que estás a utilizar em index.html.

Nº 3: Utilizar JavaScript para carregar dinamicamente o Font Awesome para o teu componente

Por fim, a forma mais prática de carregar o Font Awesome dentro do teu componente é utilizar um pouco de JavaScript:

...
class Footer extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    // Query para o DOM principal para FA
    const fontAwesome = document.querySelector('link[href*="font-awesome"]');
    const shadowRoot = this.attachShadow({ mode: 'closed' });

    // Carregar condicionalmente FA para o componente
    if (fontAwesome) {
      shadowRoot.appendChild(fontAwesome.cloneNode());
    }

    shadowRoot.appendChild(footerTemplate.content);
  }
}

customElements.define('footer-component', Footer);
components/footer.js

Este método é baseado nesta resposta no Stack Overflow (texto em inglês) e funciona de modo bastante simples. Quando o componente carrega, se um elemento link a apontar para o Font Awesome existir, então é clonado e anexado ao Shadow DOM do componente:

Screenshot-from-2021-09-12-19-31-19

Código final

Aqui está como fica o código final ao longo de todos os ficheiros, utilizando o método nº 3 para carregar o Font Awesome no componente de rodapé:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA==" crossorigin="anonymous" />
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script src="components/header.js" type="text/javascript" defer></script>
    <script src="components/footer.js" type="text/javascript" defer></script>
  </head>
  <body>
    <header-component></header-component>
    <main>
      <!-- O conteúdo da tua página -->
    </main>
    <footer-component></footer-component>
  </body>
<html>
index.html
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  color: #333;
  font-family: sans-serif;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1 0 auto;
}
style.css
const headerTemplate = document.createElement('template');

headerTemplate.innerHTML = `
  <style>
    nav {
      height: 40px;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color:  #0a0a23;
    }

    ul {
      padding: 0;
    }
    
    ul li {
      list-style: none;
      display: inline;
    }
    
    a {
      font-weight: 700;
      margin: 0 25px;
      color: #fff;
      text-decoration: none;
    }
    
    a:hover {
      padding-bottom: 5px;
      box-shadow: inset 0 -2px 0 0 #fff;
    }
  </style>
  <header>
    <nav>
      <ul>
        <li><a href="about.html">About</a></li>
        <li><a href="work.html">Work</a></li>
        <li><a href="contact.html">Contact</a></li>
      </ul>
    </nav>
  </header>
`;

class Header extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'closed' });

    shadowRoot.appendChild(headerTemplate.content);
  }
}

customElements.define('header-component', Header);
components/header.js
const footerTemplate = document.createElement('template');

footerTemplate.innerHTML = `
  <style>
    footer {
      height: 60px;
      padding: 0 10px;
      list-style: none;
      display: flex;
      flex-shrink: 0;
      justify-content: space-between;
      align-items: center;
      background-color: #dfdfe2;
    }

    ul {
      padding: 0;
    }
    
    ul li {
      list-style: none;
      display: inline;
    }
    
    a {
      margin: 0 15px;
      color: inherit;
      text-decoration: none;
    }
    
    a:hover {
      padding-bottom: 5px;
      box-shadow: inset 0 -2px 0 0 #333;
    }
    
    .social-row {
      font-size: 20px;
    }
    
    .social-row li a {
      margin: 0 15px;
    }
  </style>
  <footer>
    <ul>
      <li><a href="about.html">About</a></li>
      <li><a href="work.html">Work</a></li>
      <li><a href="contact.html">Contact</a></li>
    </ul>
    <ul class="social-row">
      <li><a href="https://github.com/my-github-profile"><i class="fab fa-github"></i></a></li>
      <li><a href="https://twitter.com/my-twitter-profile"><i class="fab fa-twitter"></i></a></li>
      <li><a href="https://www.linkedin.com/in/my-linkedin-profile"><i class="fab fa-linkedin"></i></a></li>
    </ul>
  </footer>
`;

class Footer extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const fontAwesome = document.querySelector('link[href*="font-awesome"]');
    const shadowRoot = this.attachShadow({ mode: 'closed' });

    if (fontAwesome) {
      shadowRoot.appendChild(fontAwesome.cloneNode());
    }

    shadowRoot.appendChild(footerTemplate.content);
  }
}

customElements.define('footer-component', Footer);
components/footer.js

Para terminar

Abordamos muita coisa aqui. Podes já ter decidido simplesmente utilizar React ou Handlebars.js em vez disso.

São ambas óptimas opções!

Mesmo assim, para um projeto mais pequeno onde vais precisar apenas de alguns componentes reutilizáveis, uma biblioteca completa ou linguagem de template pode ser demasiado.

Esperançosamente, agora tens a confiança para criar os teus próprios componentes HTML reutilizáveis. Agora, vai lá e cria algo excelente (e reutilizável).