Skip to main content

Command Palette

Search for a command to run...

Building a Reusable Styled Table Using Web Components (Step-by-Step)

Updated
4 min read

Web Components allow us to create reusable, framework-free UI components using native browser APIs. In this article, we’ll build a styled table component that:

  • Works like a custom HTML tag

  • Has scoped CSS using Shadow DOM

  • Accepts headers via HTML attributes

  • Accepts data via JavaScript

No React. No Vue. Just pure JavaScript.


Step 1: index.html – Using the Custom Element

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Web Components Table</title>
</head>
<body>

  <styled-table
    id="users"
    data-headers="ID, Username, Country">
  </styled-table>

  <script type="module" src="./index.js"></script>
</body>
</html>

Explanation

  • <styled-table> is not a native HTML tag — we’ll define it ourselves.

  • data-headers is a configuration attribute that controls table headers.

  • type="module" is required to use ES module imports in the browser.

At this point, the browser doesn’t know what <styled-table> is — we’ll fix that next.


Step 2: index.js – Registering the Web Component

import { StyledTable } from "./components/styled-table.js";

customElements.define("styled-table", StyledTable);

const usersTable = document.getElementById("users");

const data = [
  ["8831", "dcode", "Australia"],
  ["8832", "paras", "India"]
];

usersTable.data = data;

Explanation

  • customElements.define() registers a new HTML tag.

  • Custom element names must contain a dash.

  • Once registered, the browser understands <styled-table>.

We also assign data using:

usersTable.data = ...

This works because the component exposes a setter, which we’ll implement later.


Step 3: Creating the Custom Element Class

components/styled-table.js

export class StyledTable extends HTMLElement {

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

Explanation

  • extends HTMLElement tells the browser this is a custom HTML element.

  • super() is required to properly initialize the element.

  • attachShadow({ mode: "open" }) creates a Shadow DOM, which:

    • Isolates HTML and CSS

    • Prevents global styles from leaking in

    • Keeps component styles scoped


Step 4: Lifecycle – connectedCallback()

  connectedCallback() {
    const headers = this.dataset.headers
      ?.split(",")
      .map(h => h.trim()) || [];

    this.shadowRoot.innerHTML = `
      <link rel="stylesheet" href="/components/sl.css">

      <table>
        <thead>
          <tr>
            ${headers.map(h => `<th>${h}</th>`).join("")}
          </tr>
        </thead>
        <tbody></tbody>
      </table>
    `;
  }

Explanation

  • connectedCallback() runs when the element is added to the DOM.

  • this.dataset.headers reads data-headers from HTML.

  • split(",") + trim() converts the string into a clean array.

  • We generate <th> elements dynamically using map().

This makes the component configurable from HTML.


Step 5: Scoped Styling with Shadow DOM

components/sl.css

table {
  border-collapse: collapse;
  font-family: Inter, sans-serif;
  font-size: 0.85rem;
  min-width: 400px;
}

thead tr {
  background-color: #009578;
  color: white;
  text-align: left;
}

th, td {
  padding: 6px 12px;
  font-weight: normal;
}

tbody tr {
  border-bottom: 1px solid #ddd;
}

tbody tr:nth-of-type(even) {
  background-color: #f3f3f3;
}

Explanation

  • The CSS is loaded inside the Shadow DOM.

  • These styles only affect this component.

  • No class names are needed — we can target elements directly.

This is one of the biggest advantages of Web Components.


Step 6: Passing Data via a Public API

Back in styled-table.js:

  set data(data) {
    const tbody = this.shadowRoot.querySelector("tbody");

    tbody.innerHTML = "";

    const rows = data.map(rowData => {
      const row = document.createElement("tr");

      const cells = rowData.map(cellData => {
        const cell = document.createElement("td");
        cell.textContent = cellData;
        return cell;
      });

      row.append(...cells);
      return row;
    });

    tbody.append(...rows);
  }
}

Explanation

  • We expose a setter called data.

  • This allows clean usage like:

      table.data = [...]
    
  • The component controls how the DOM is updated.

  • External code never touches internal markup.

This pattern makes the component safe, reusable, and maintainable.


Final Result

We now have:

  • A custom HTML tag

  • Scoped HTML and CSS

  • Dynamic headers via attributes

  • Dynamic rows via JavaScript

  • Zero frameworks

All using native browser APIs.


When Should You Use Web Components?

Web Components are perfect for:

  • Design systems

  • Reusable widgets

  • Framework-agnostic UI libraries

  • Projects where you want long-term maintainability

They can even coexist with React, Vue, or plain HTML.


Closing Thoughts

This example shows how powerful Web Components can be when used correctly. With Shadow DOM, lifecycle methods, and clean public APIs, we can build components that scale just like framework components — without the framework.