Building a Reusable Styled Table Using Web Components (Step-by-Step)
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-headersis 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 HTMLElementtells 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.headersreadsdata-headersfrom HTML.split(",") + trim()converts the string into a clean array.We generate
<th>elements dynamically usingmap().
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.