Categories
HTML JavaScript

What’s new in Lighthouse 6

Lighthouse is a tool made by Google to let us measure the performance metrics of our browser apps.

Version 6 is the latest version of the software.

As with other versions, it’s available as part of Chrome or as an NPM package to let us run it automatically to check those performance metrics when the software is being built for continuous integration.

In this article, we’ll look at the latest features of Lighthouse 6.

New Metrics

Lighthouse 6 added some new metrics for measuring the performance of our browser apps.

Largest Contentful Paint (LCP)

One of them is the Largest Contentful Paint (LCP). It’s a measurement of perceived loading experience.

It captures how long it takes to load all the parts of our page.

Like with First Contentful Paint (FCP), we don’t want this to take too long.

If it’s more than 2.5 seconds, then it’s too long and we’ve to make our web app speedier.

Cumulative Layout Shift (CLS)

Another new metric is the Cumulative Layout Shift or CLS.

This is a measurement of visual stability.

Users don’t want a web page to have content that moves around too much.

Jumpy pages make it easy for users to do something they don’t want to do accidentally.

Therefore, any unexpected shift is annoying.

It’s measured by the following formula:

layout shift score = impact fraction * distance fraction

Impact fraction is the measurement of how unstable some element is.

It’s a percentage change of the distance from the original position to the new position.

Distance fraction is the distance that an element moved relative to the viewport.

It’s the great distance of any unstable element that has moved in the frame.

A score below 0.1 is considered good.

These are welcome changes since slowing loading and jumpy pages are always frustrating for users.

Total Blocking Time (TBT)

Total Blocking Time is another measurement of responsiveness.

It’s the total duration of when the main thread is blocked long enough to prevent input responsiveness.

It’s the total time between FCP and Time to Interactive.

Time to Interactive is the time to take to load the page until when we can interact with the page.

Performance Score Update

In Lighthouse 6, the performance score is calculated from the weighted blend of multiple performance metrics to summarize page speed.

The metrics are weighted as follows according to https://web.dev/lighthouse-whats-new-6.0/#new-metrics:

Phase Metric Name Metric Weight
Early (15%) First Contentful Paint (FCP) 15%
Mid (40%) Speed Index (SI) 15%
Largest Contentful Paint (LCP) 25%
Late (15%) Time To Interactive (TTI) 15%
Main Thread (25%) Total Blocking Time (TBT) 25%
Predictability (5%) Cumulative Layout Shift (CLS) 5%

The weights are changed in all the categories to takes into account the new metrics.

In version 5, the weights are as follows:

Phase Metric Name Metric Weight
Early (23%) First Contentful Paint (FCP) 23%
Mid (34%) Speed Index (SI) 27%
First Meaningful Paint (FMP) 7%
Finished (46%) Time to Interactive (TTI) 33%
First CPU Idle (FCI) 13%
Main Thread Max Potential FID 0%

TTI’s weight decreased according to user feedback. It’s very variable so reducing its weight will decrease its variability.

FCP is also decreased in weight since it doesn’t give us the full picture of loading.

The first CPU Idle metric is deprecated since it isn’t as direct as other metrics for interactivity.

With Lighthouse 6, we can use the Lighthouse Scoring Calculator to make our calculations.

New Audits Tools

Lighthouse 6 can audit unused JavaScript.

It’s included since 2017, but it’s disabled by default to keep Lighthouse fast.

Now that the tool is more efficient, it’s turned on by default.

It also has some new audit features to audit for some accessibility attributes.

They include:

  • aria-hidden-body
  • aria-hidden-focus
  • aria-input-field-name
  • aria-toggle-field-name
  • form-field-multiple-labels
  • heading-order
  • duplicate-id-active
  • duplicate-id-aria

aria-hidden-body makes sure aria-hidden=’true’ is in the body element

aria-hidden-focus checks for aria-hidden in focusable elements.

aria-input-field-name checks for aria attributes in input field names.

aria-toggle-field-name checks for aria attributes in a toggle field.

Form fields shouldn’t have multiple labels.

Headings should be sequentially descending order.

And there shouldn’t be any duplicate aria IDs.

Lighthouse 6 check for all those so that we won’t confuse users that are impaired.

Maskable icons are a new icon format that supports progressive web apps.

It’ll check for them so that we get good looking icons everywhere.

It also checks for the meta charset element to our HTML.

We also should add a Content-Type response header which it also checks for.

It specifies the character encoding explicitly so browsers won’t be confused.

With this declaration, browsers can’t render our page wrong.

Otherwise, it can render our page wrong and make the user experience poor.

Source Location Links

Lighthouse 6 provides us with the lines of code in our code that are causing audits to fail.

It has links to look through them in the Sources panel.

Experimental Features

Experimental features in Lighthouse 6 that are worth looking at include the ability to detect duplicate modules in JavaScript bundles.

It can also detect extra polyfills that we don’t need in modern browsers.

And it can detect unused JavaScript and report them by modules.

There’s also a visualization of all things in a module that require changes to improve the audit score.

We can enable them with the --preset experimental switch.

For example, we can run:

lighthouse https://example.com --view --preset experimental

to run Lighthouse in the command line with the experimental features on.

Lighthouse CI

Lighthouse CI is a Node CLI program and a server that lets us run Lighthouse audits for every commit.

It can easily be integrated into the continuous integration pipeline.

It supports many CI providers like Travis, Circle, GitLab, and Github Actions.

Docker images are provided to let us make the work of setting up the build pipeline with Lighthouse easy.

We can also install it manually.

To do that, we run:

npm install -g lighthouse

Then we can run it by running:

lighthouse <url>

Where url is the URL of our page.

Then we can change some options like logging and other configuration.

Logging can be set with --verbose or --quiet.

Other configuration options include changing the port, host name, emulate desktop, mobile, etc.

The full list of options are at https://www.npmjs.com/package/lighthouse#using-lighthouse-in-chrome-devtools

For instance, we can run Lighthouse and output to JSON by running:

lighthouse --output json

Renamed Chrome DevTools Panel

The Audits panel has been renamed to the Lighthouse panel in chrome.

This makes it easier to find.

Mobile Emulation

We can test mobile performance with emulation of slow network and CPU conditions.

Device screen emulation can also be done.

Lighthouse 6 has changed the reference device to the Moto G4.

Browser Extension

There’s a Chrome extension for Lighthouse.

This lets us run it locally in addition to what’s in Chrome itself.

It uses the PageSpeed Insights API.

The API has many limitations.

It can’t audit non-public websites since it runs from a remote server and not locally.

It’s also not guaranteed to have the latest Lighthouse release.

In Chrome, we should use the Lighthouse section of the dev tools to do our audits.

Or we can use the Node package version.

Conclusion

Lighthouse 6 is an even better version of Lighthouse.

It has new metrics to audit the performance of an app from the user’s perspective.

The weights to calculate the final performance score is changed from user feedback.

Also, it can look through our code to find where the problems originate.

This is very helpful when it comes to fixing problems easily.

With the Node package, we can automate our auditing easily.

Then the problems that needed to be fixed will be apparent to us all the time.

Categories
Angular Web Components

Creating Web Components with Angular

Angular is a popular front-end framework made by Google. Like other popular front-end frameworks, it uses a component-based architecture to structure apps.

In this article, we’ll look at how to create Angular Elements, which are packaged as Web Components.

Angular Elements

Web Components are supported by most browsers like Chrome, Firefox, Opera or Safari.

We can transform Angular components to Web Components to make all the Angular infrastructure available to the browser.

Features like data-binding and other Angular functionalities are mapped to their HTML equivalents.

Creating and Using Custom Elements

We can create a Web Component by creating an Angular component, then building it into a Web Component.

To create a Web Component with Angular, we have to do a few things.

First, we create a component to build into Web Components. Then we have to set the component we created as the entry point.

Then we can add it to the DOM.

We’ll make a custom component to get a joke. To do this, we first run:

ng g component customJoke
ng g service joke

to create our component and service to get our joke and display it.

Then we run:

ng add @angular/element

to add the Angular Element files to create our Web Component.

Then injoke.service.ts , we add:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class JokeService {

  constructor() { }

  async getJokeById(id: number) {
    const response = await fetch(`http://api.icndb.com/jokes/${id}`)
    const joke = await response.json();
    return joke;
  }
}

The code above gets a joke from the Chuck Norris API by ID.

Next, we write our component code as follows:

app.module.ts :

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { AppComponent } from './app.component';
import { CustomJokeComponent } from './custom-joke/custom-joke.component';
import { JokeService } from './joke.service';

@NgModule({
  declarations: [
    CustomJokeComponent,
    AppComponent
  ],
  imports: [
    BrowserModule,
  ],
  providers: [JokeService],
  bootstrap: [AppComponent],
  entryComponents: [CustomJokeComponent]
})
export class AppModule {

}

In AppModule , we add CustomJokeComponent to entryComponents so that it’ll be the entry point component instead of AppComponent .

It’ll load when custom-joke element is created.

app.component.ts :

import { Component, Injector } from '@angular/core';
import { createCustomElement, WithProperties, NgElement } from '@angular/elements';
import { CustomJokeComponent } from './custom-joke/custom-joke.component';

@Component({
  selector: 'app-root',
  template: ''
})
export class AppComponent {
  constructor(injector: Injector) {
    const JokeElement = createCustomElement(CustomJokeComponent, { injector });
    customElements.define('custom-joke', JokeElement);
    this.showAsElement(20);
  }

  showAsElement(id: number) {
    const jokeEl: WithProperties<CustomJokeComponent> = document.createElement('custom-joke') as any;
    jokeEl.id = id;
    document.body.appendChild(jokeEl as any);
  }
}

The code in the constructor creates the custom component and attaches it to the DOM with our showAsElement method.

createCustomElement is from our @angular/element code.

The showAsElement method loads our custom-joke Web Component that we defined earlier.

custom-joke.component.ts :

import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core';
import { JokeService } from '../joke.service';

@Component({
  selector: 'custom-joke',
  template: `<p>{{joke?.value?.joke}}</p>`,
  styles: [`p { font-size: 20px }`],
  encapsulation: ViewEncapsulation.Native
})
export class CustomJokeComponent implements OnInit {
  @Input() id: number = 1;
  joke: any = {};

  constructor(private jokeService: JokeService) { }

  async ngOnInit() {
    this.joke = await this.jokeService.getJokeById(this.id)
  }

}

We put everything in one file so they can all be included in our custom-joke Web Component.

The @Input will be converted to an attribute that we can pass a number into and get the joke by its ID.

We leave custom-joke.component.html and app.component.html blank.

Conclusion

We use the @angular/element package to create a Web Component that we can use.

The difference is that we include the template and styles inline.

Also, we have to register the component and attach it to the DOM.

Categories
Web Components

Creating Web Components — Templates and Slots

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable UI blocks that we can use in multiple places.

In this article, we’ll look at how to use templates and slots with Web Components.

Templates

We can use the template element to add content that’s not rendered in the DOM. This lets us incorporate them into any other element by writing the following HTML:

<template>
  <p>Foo</p>
</template>

Then we can write the following JavaScript to incorporate it into the page:

const template = document.querySelector('template');
const templateContent = template.content;
document.body.appendChild(templateContent);

We should see ‘Foo’ on the page when we load the page. The p element should be showing when we inspect the code for ‘Foo’. This confirms that the markup isn’t affected by the template element.

Using Templates with Web Components

We can also use them with Web Components. To use it, we can get the template element in the class for the Web Component just like we do outside the Web Component.

For example, we can write:

customElements.define('foo-paragraph',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.querySelector('template');
      let templateContent = template.content;
      const shadowRoot = this.attachShadow({
          mode: 'open'
        })
        .appendChild(templateContent.cloneNode(true));
    }
  })

In the code above, we get the template element and the call cloneNode on templateContent which has template.content as the value to get the template’s content.

Then we call cloneNode to clone the template.content node’s children in addition to the node itself. This is indicated by the true argument of the cloneNode function call.

Finally, we attached the cloned templateContent into the shadow root of the Web Component with the attachShadow and appendChild methods.

We can also include styling inside a template element, so we can reuse it as we do with the HTML markup.

For instance, we can change the template element to:

<template>
  <style>
    p {
      color: white;
      background-color: gray;
      padding: 5px;
    }

  </style>
  <p>Foo</p>
</template>

Then we should see:

Photo by Zoë Reeve on Unsplash

Slots

To make our templates more flexible, we can add slots to them to display content as we wish instead of static content. With slots, we can pass in content between our Web Component opening and closing tags.

It has more limited support than templates. Slots can be used with Chrome 53 or later, Opera since version 40, Firefox since version 59, and not supported in Edge.

For example, we can add some slots by writing:

<template>
  <slot name="foo">Default text for foo</slot>
  <slot name="bar">Default text for bar</slot>
  <slot name="baz">Default text for baz</slot>
</template>

Then given that we have the same code as before for defining the foo-paragraph element in JavaScript, we can use it as follows:

<foo-paragraph>
  <p slot='foo'>foo</p>
  <p slot='bar'>bar</p>
  <p slot='baz'>baz</p>
</foo-paragraph>

The output then will be:

foo

bar

baz

If we omit the elements inside the foo-paragraph tags, we get the default text:

Default text for foo Default text for bar Default text for baz

In the code above, we name the slots with the name attribute in the template element, then in our Web Component, we fill in the slots that we defined by using the slot attribute.

We can style them like any other template element. To style them, we select the elements to be styled by using the selector for the elements that we’ll fill into the slots.

For example, we can write:

<template>
  <style>
    ::slotted(p) {
      padding: 5px;
      background-color: black;
      color: white;
    }

    ::slotted(p[slot='foo']) {
      background-color: gray;
    }
  </style>
  <slot name="foo">Default text for foo</slot>
  <slot name="bar">Default text for bar</slot>
  <slot name="baz">Default text for baz</slot>
</template>

<foo-paragraph>
  <p slot='foo'>foo</p>
  <p slot='bar'>bar</p>
  <p slot='baz'>baz</p>
</foo-paragraph>

The ::slotted pseudo-element represents elements that has been placed into a slot inside the HTML template. Therefore, we can use it to select the slot elements that we want to style.

In the argument of ::slotted , we can pass in the element that we want to style if they were passed into the slot. This means that only p elements that are passed in will have styles applied to them.

::slotted(p[slot=’foo’]) means that a p element with slot attribute values foo will have styling applied to it.

Then we should get the following after applying the styles:

Templates and slots let us create reusable components that we can use in Web Components. The template element does not show up during render so we can put them anywhere and use them as we wish in any Web Component.

With slots, we have more flexibility when creating Web Components since we can create components that we can insert other elements into to fill the slots.

In any case, we can apply styles to the elements as we wish. We have the ::slotted pseudoelement to apply styles to specific elements in slots.

Categories
Web Components

Creating Web Components — Lifecycle Callbacks

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable UI blocks that we can use in multiple places.

In this article, we’ll look at the lifecycle hooks of Web Components and how we use them.

Lifecycle Hooks

Web Components have their own lifecycle. The following events happen in a Web Component’s lifecycle:

  • Element is inserted into the DOM
  • Updates when a UI event is being triggered
  • Element deleted from the DOM

Web Component has lifecycle hooks are callback functions that capture these lifecycle events and let us handle them accordingly.

They let us handle these events without creating our own system to do so. Most JavaScript frameworks provide the same functionality, but Web Components is a standard so we don’t need to load extra code to be able to use them.

The following lifecycle hooks are in a web component:

  • constructor()
  • connectedCallback()
  • disconnectedCallback()
  • attributeChangedCallback(name, oldValue, newValue)
  • adoptedCallback()

constructor()

The constructor() is called when the Web Component is created. It’s called when we create the shadow DOM and it’s used for setting up listeners and initialize a component’s state.

However, it’s not recommended that we run things like rendering and fetching resources here. The connectedCallback is better for these kinds of tasks.

Defining a constructor is optional for ES6 classes, but an empty one will be created when it’s undefined.

When creating the constructor, we’ve to call super() to call the class that the Web Component class extends.

We can have return statements in there and we can’t use document.write() or document.open() in there.

Also, we can’t gain attributes or children in the constructor method.

connectedCallback()

connectedCallback() method is called when an element is added to the DOM. We can be sure that the element is available to the DOM when this method is called.

This means that we can safely set attributes, fetch resources, run set up code or render templates.

disconnectedCallback()

This is called when the element is removed from the DOM. Therefore, it’s an ideal place to add cleanup logic and to free up resources. We can also use this callback to:

  • notify another part of an application that the element is removed from the DOM
  • free resources that won’t be garbage collected automatically like unsubscribing from DOM events, stop interval timers, or unregister all registered callbacks

This hook is never called when the user closes the tab and it can be trigger more than once during its lifetime.

attributeChangedCallback(attrName, oldVal, newVal)

We can pass attributes with values to a Web Component like any other attribute:

<custom-element
  foo="foo"
  bar="bar"
  baz="baz">
</custom-element>

In this callback, we can get the value of the attributes as they’re assigned in the code.

We can add a static get observedAttributes() hook to define what attribute values we observe. For example, we can write:

class CustomElement extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({
      mode: 'open'
    });
  }

  static get observedAttributes() {
    return ['foo', 'bar', 'baz'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`${name}'s value has been changed from ${oldValue} to ${newValue}`);
  }
}

customElements.define('custom-element', CustomElement);

Then we should the following from the console.log :

foo's value has been changed from null to foo
bar's value has been changed from null to bar
baz's value has been changed from null to baz

given that we have the following HTML:

<custom-element foo="foo" bar="bar" baz="baz">
</custom-element>

We get this because we assigned the values to the foo, bar, and baz attributes in the HTML with the values of the same name.

adoptedCallback()

The adoptedCallback is called when we call document.adoptNode with the element passed in. It only occurs when we deal with iframes.

The adoptNode method is used to transfer a node from one document to another. An iframe has another document, so it’s possible to call this with iframe’s document object.

Example

We can use it to create an element with text that blinks as follows:

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

  connectedCallback() {
    const shadow = this.attachShadow({
      mode: 'open'
    });
    this.span = document.createElement('span');
    this.span.textContent = this.getAttribute('text');
    const style = document.createElement('style');
    style.textContent = 'span { color: black }';
    this.intervalTimer = setInterval(() => {
      let styleText = this.style.textContent;
      if (style.textContent.includes('red')) {
        style.textContent = 'span { color: black }';
      } else {
        style.textContent = 'span { color: red }';
      }

}, 1000)
    shadow.appendChild(style);
    shadow.appendChild(this.span);
  }

  disconnectedCallback() {
    clearInterval(this.intervalTimer);
  }

  static get observedAttributes() {
    return ['text'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'text') {
      if (this.span) {
        this.span.textContent = newValue;
      }

    }
  }
}

customElements.define('blink-element', BlinkElement);

In the connectedCallback , we have all the code for initializing the element. It includes adding a span for the text that we passed into the text attribute as its value, and the styles for blinking the text. The style starts with the creation of the style element and then we added the setInterval code to blink the text by changing the color of the span from red to black and vice versa.

Then we attached the nodes to the shadow DOM which we created at the beginning.

We have the observedAttributes static method to set the attributes to watch for. This is used by the attributeChangedCallback to watch for the value of the text attribute and set it accordingly by getting the newValue .

To see the attributeChangedCallback in action, we can create the element dynamically and set its attribute as follows:

const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
blink2.setAttribute('text', 'baz');

Then we should bar and baz if we log newValue in the attributeChangedCallback hook.

Finally, we have the disconnectedCallback which is run when the component is removed. For example, when we remove an element with removeChild as in the following code:

const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
document.body.removeChild(blink2);

The lifecycle hooks allow us to handle various DOM events inside and outside the shadow DOM. Web Components have hooks for initializing, removing, and attribute changes. We can select which attributes to watch for changes.

There’re also hooks for adopting elements in another document.

Categories
Web Components

Introduction to Creating Web Components

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable components that we can use in multiple places.

Web Components are also isolated from other pieces of code so it’s harder to accidentally modify them from other pieces of code and create conflicting code.

In this article, we’ll look at the different parts of a Web Component and how to create basic ones.

Parts of a Web Component

A Web Component has 3 main parts. Together they encapsulate the functionality that can be reused whenever we like without fear of code conflicts.

  • custom element — a set of JavaScript APIs that allow us to define custom elements and their behavior, which can be used as we desired in the UI
  • shadow DOM — set of JavaScript APIs for attaching the encapsulated shadow DOM tree of the element. It’s rendered separately from the main document DOM. this keeps the element’s features private, so they can be scripted without the fear of conflicting with other parts of the document
  • HTML templates — the template or slot elements enable us to write markup templates that aren’t displayed on the rendered page. They can be reused multiple times as the basis of the custom elements structure.

Creating Web Components

To create Web Components, we do the following steps:

  1. Create a class or function to specify the web component functionality.
  2. Register our new custom element using the CustomElementRegistry.define() method, passing the element name to be defined and the class or function which the functionality is specified, and optionally what element it inherits from
  3. Attach a shadow DOM to the custom element using the Element.attachSHadow() method. Add child elements, event listeners, etc. to the shadow DOM using normal DOM methods
  4. Define HTML templates using the template and slot tags. We use regular DOM methods to clone the template and attach it to our shadow DOM.
  5. Use custom element wherever we like on our page like any other regular HTML element

Basic Examples

The CustomElementRegistry keeps a list of custom elements that are defined. This object lets us register new custom elements on a page and return information about what custom elements are registered etc.

To register a new custom element on a page, we use the CustomElementRegistry.define() method. It takes the following arguments:

  • a string representing the name of the element. A dashed is required to be used in them. They can’t be single words.
  • a class object that defines the behavior of the element
  • an optional argument containing an extends property which specifies the built-in element our element inherits from if any

We can define a custom element as follows:

class WordCount extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({
      mode: 'open'
    });
    const span = document.createElement('span');
    span.textContent = this.getAttribute('text').split(' ').length;
    const style = document.createElement('style');
    style.textContent = 'span { color: red }';
    shadow.appendChild(style);
    shadow.appendChild(span);
  }
}

customElements.define('word-count', WordCount);

In the code above, we attach a shadow DOM to our document by calling attachShadow . mode set to 'open' means that the shadow root is accessible from JavaScript outside the root. It can also be 'closed' which means the opposite.

Then we create a span element, where we set the text content to the text attribute’s length after we split the words in it wherever there’s a space.

Next, we created a style element and then set the content to color: red .

Finally, we attached them both to the shadow DOM root.

Notice that in our class, we extended the HTMLElement . We’ve to do this to define a custom element.

We can use our new element as follows:

<word-count text='Hello world.'></word-count>

There’re 2 types of custom elements. They are:

  • autonomous custom elements — which are standalone and don’t inherit from standard HTML elements. We can create them by referencing the name directly. For example, we can write <word-count>, or document.createElement("word-count").
  • customized built-in elements — these inherit from basic HTML elements. We can extend one of the built-in HTML elements to create these kinds of elements. We can use them by writing <p is="word-count">, or document.createElement("p", { is: "word-count" }).

The kind of custom element we created before are autonomous custom elements. We can create customized elements by writing:

class WordCount extends HTMLParagraphElement {
  constructor() {
    super();
    const shadow = this.attachShadow({
      mode: 'open'
    });
    const span = document.createElement('span');
    span.textContent = this.getAttribute('text').split(' ').length;
    const style = document.createElement('style');
    style.textContent = 'span { color: red }';
    shadow.appendChild(style);
    shadow.appendChild(span);
  }
}

customElements.define('word-count', WordCount, {
  extends: 'p'
});

Then put in our page by writing:

<p is='word-count' text='Hello world.'></p>

As we can see, autonomous custom elements and customized built-in elements aren’t that different. The only differences are that we extend HTMLParagraphElement instead of HTMLElement .

Then we used the is attribute to reference the custom element instead of using the element name in the customized built-in element.

Internal vs. External Styles

We can also reference external styles as follows instead of using internal styles as we had above. For example, we can write:

const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
shadow.appendChild(linkElem);

This references styles from style.css . We created a link element just like we do in HTML and normal DOM manipulation.

Creating Web Components is simple. We just have to define a class or function for the functionality and then put it in the custom elements registry by using the customElements.define method.

We can extend existing elements like p elements or create ones from scratch. Also, we can add internal styles or reference external ones by creating a link element and referencing external files.