Building Enterprise UI Systems in Vanilla JS
Build enterprise-grade UI systems in vanilla JavaScript. Covers design token systems, accessible component libraries, complex form architectures, data table engines, theming, and component composition patterns for large-scale applications.
Enterprise applications demand UI systems that scale across teams. This guide covers design tokens, accessible components, complex forms, data tables, and theming built entirely in vanilla JavaScript without framework dependencies.
For state management patterns that power these systems, see Vanilla JS State Management for Advanced Apps.
Design Token System
class DesignTokens {
#tokens = new Map();
#themes = new Map();
#activeTheme = null;
#root;
constructor() {
this.#root = document.documentElement;
}
define(category, tokens) {
for (const [name, value] of Object.entries(tokens)) {
const tokenKey = `--${category}-${name}`;
this.#tokens.set(tokenKey, value);
}
return this;
}
createTheme(name, overrides) {
this.#themes.set(name, overrides);
return this;
}
applyTheme(name) {
// Reset to base tokens
for (const [key, value] of this.#tokens) {
this.#root.style.setProperty(key, value);
}
// Apply theme overrides
const theme = this.#themes.get(name);
if (theme) {
for (const [key, value] of Object.entries(theme)) {
this.#root.style.setProperty(key, value);
}
}
this.#activeTheme = name;
document.dispatchEvent(
new CustomEvent("themechange", { detail: { theme: name } })
);
}
getToken(key) {
return getComputedStyle(this.#root).getPropertyValue(key).trim();
}
getActiveTheme() {
return this.#activeTheme;
}
}
// Define the system
const tokens = new DesignTokens();
tokens
.define("color", {
primary: "#2563eb",
"primary-hover": "#1d4ed8",
secondary: "#64748b",
success: "#16a34a",
error: "#dc2626",
warning: "#d97706",
surface: "#ffffff",
"surface-elevated": "#f8fafc",
text: "#0f172a",
"text-muted": "#64748b",
border: "#e2e8f0",
})
.define("spacing", {
xs: "0.25rem",
sm: "0.5rem",
md: "1rem",
lg: "1.5rem",
xl: "2rem",
"2xl": "3rem",
})
.define("font", {
"size-sm": "0.875rem",
"size-base": "1rem",
"size-lg": "1.125rem",
"size-xl": "1.25rem",
"size-2xl": "1.5rem",
"weight-normal": "400",
"weight-medium": "500",
"weight-bold": "700",
})
.define("radius", {
sm: "0.25rem",
md: "0.375rem",
lg: "0.5rem",
full: "9999px",
});
// Dark theme
tokens.createTheme("dark", {
"--color-surface": "#0f172a",
"--color-surface-elevated": "#1e293b",
"--color-text": "#f1f5f9",
"--color-text-muted": "#94a3b8",
"--color-border": "#334155",
"--color-primary": "#3b82f6",
});
tokens.applyTheme("light");Accessible Component Base
class UIComponent {
#element;
#id;
#announcer;
static #counter = 0;
static #liveRegion = null;
constructor(tag, role) {
this.#id = `ui-${++UIComponent.#counter}`;
this.#element = document.createElement(tag);
this.#element.id = this.#id;
if (role) this.#element.setAttribute("role", role);
if (!UIComponent.#liveRegion) {
UIComponent.#liveRegion = document.createElement("div");
UIComponent.#liveRegion.setAttribute("aria-live", "polite");
UIComponent.#liveRegion.setAttribute("aria-atomic", "true");
UIComponent.#liveRegion.className = "sr-only";
document.body.appendChild(UIComponent.#liveRegion);
}
}
get element() {
return this.#element;
}
get id() {
return this.#id;
}
announce(message) {
UIComponent.#liveRegion.textContent = "";
requestAnimationFrame(() => {
UIComponent.#liveRegion.textContent = message;
});
}
setAriaLabel(label) {
this.#element.setAttribute("aria-label", label);
return this;
}
setAriaDescribedBy(id) {
this.#element.setAttribute("aria-describedby", id);
return this;
}
trapFocus() {
const focusable = this.#element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
const handler = (e) => {
if (e.key !== "Tab") return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
this.#element.addEventListener("keydown", handler);
first?.focus();
return () => this.#element.removeEventListener("keydown", handler);
}
mount(container) {
container.appendChild(this.#element);
return this;
}
unmount() {
this.#element.remove();
}
}
// Accessible Dialog
class Dialog extends UIComponent {
#overlay;
#releaseFocus;
#previousFocus;
#onClose;
constructor(title, content, onClose) {
super("div", "dialog");
this.#onClose = onClose;
this.element.setAttribute("aria-modal", "true");
this.element.setAttribute("aria-labelledby", `${this.id}-title`);
this.element.className = "dialog";
this.element.innerHTML = `
<div class="dialog-header">
<h2 id="${this.id}-title">${title}</h2>
<button class="dialog-close" aria-label="Close dialog">×</button>
</div>
<div class="dialog-body">${content}</div>
<div class="dialog-footer">
<button class="btn btn-secondary" data-action="cancel">Cancel</button>
<button class="btn btn-primary" data-action="confirm">Confirm</button>
</div>
`;
this.element.querySelector(".dialog-close").addEventListener("click", () => this.close());
this.element.addEventListener("keydown", (e) => {
if (e.key === "Escape") this.close();
});
}
open() {
this.#previousFocus = document.activeElement;
this.#overlay = document.createElement("div");
this.#overlay.className = "dialog-overlay";
this.#overlay.addEventListener("click", () => this.close());
document.body.appendChild(this.#overlay);
this.mount(document.body);
this.#releaseFocus = this.trapFocus();
this.announce("Dialog opened");
}
close() {
this.#releaseFocus?.();
this.#overlay?.remove();
this.unmount();
this.#previousFocus?.focus();
this.announce("Dialog closed");
this.#onClose?.();
}
bindConfirm(handler) {
this.element.querySelector('[data-action="confirm"]')
.addEventListener("click", () => {
handler();
this.close();
});
}
}Complex Form Architecture
class FormField {
#config;
#element;
#input;
#errorEl;
#value;
#touched = false;
#dirty = false;
constructor(config) {
this.#config = config;
this.#value = config.defaultValue ?? "";
this.#element = document.createElement("div");
this.#element.className = "form-field";
this.#render();
}
#render() {
const { name, label, type, required, help } = this.#config;
this.#element.innerHTML = `
<label for="field-${name}">
${label}${required ? ' <span class="required" aria-hidden="true">*</span>' : ""}
</label>
${this.#renderInput()}
${help ? `<div class="field-help" id="help-${name}">${help}</div>` : ""}
<div class="field-error" id="error-${name}" role="alert"></div>
`;
this.#input = this.#element.querySelector(`#field-${name}`);
this.#errorEl = this.#element.querySelector(`#error-${name}`);
if (help) {
this.#input.setAttribute("aria-describedby", `help-${name}`);
}
this.#input.addEventListener("blur", () => {
this.#touched = true;
this.validate();
});
this.#input.addEventListener("input", () => {
this.#dirty = true;
this.#value = this.#input.value;
if (this.#touched) this.validate();
});
}
#renderInput() {
const { name, type, placeholder, options } = this.#config;
switch (type) {
case "textarea":
return `<textarea id="field-${name}" name="${name}" placeholder="${placeholder || ""}">${this.#value}</textarea>`;
case "select":
return `<select id="field-${name}" name="${name}">
<option value="">Select...</option>
${(options || []).map((o) => `<option value="${o.value}" ${o.value === this.#value ? "selected" : ""}>${o.label}</option>`).join("")}
</select>`;
case "checkbox":
return `<input type="checkbox" id="field-${name}" name="${name}" ${this.#value ? "checked" : ""}>`;
default:
return `<input type="${type || "text"}" id="field-${name}" name="${name}" placeholder="${placeholder || ""}" value="${this.#value}">`;
}
}
validate() {
const rules = this.#config.rules || [];
const errors = [];
for (const rule of rules) {
const error = rule(this.#value);
if (error) errors.push(error);
}
if (errors.length > 0) {
this.#errorEl.textContent = errors[0];
this.#element.classList.add("has-error");
this.#input.setAttribute("aria-invalid", "true");
this.#input.setAttribute("aria-errormessage", `error-${this.#config.name}`);
} else {
this.#errorEl.textContent = "";
this.#element.classList.remove("has-error");
this.#input.removeAttribute("aria-invalid");
this.#input.removeAttribute("aria-errormessage");
}
return errors;
}
getValue() { return this.#value; }
getElement() { return this.#element; }
isTouched() { return this.#touched; }
isDirty() { return this.#dirty; }
reset() {
this.#value = this.#config.defaultValue ?? "";
this.#touched = false;
this.#dirty = false;
this.#render();
}
}
// Validation rules
const rules = {
required: (msg = "Required") => (v) => (!v || !v.trim() ? msg : null),
minLength: (min) => (v) => (v && v.length < min ? `At least ${min} characters` : null),
maxLength: (max) => (v) => (v && v.length > max ? `At most ${max} characters` : null),
email: () => (v) => (v && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? "Invalid email" : null),
pattern: (re, msg) => (v) => (v && !re.test(v) ? msg : null),
};Data Table Engine
class DataTable {
#container;
#columns;
#data = [];
#sortColumn = null;
#sortDirection = "asc";
#currentPage = 1;
#pageSize = 10;
#selectedRows = new Set();
#filters = {};
constructor(container, columns) {
this.#container = container;
this.#columns = columns;
this.#container.setAttribute("role", "grid");
this.#container.setAttribute("aria-label", "Data table");
}
setData(data) {
this.#data = [...data];
this.#currentPage = 1;
this.render();
return this;
}
#getProcessedData() {
let result = [...this.#data];
// Apply filters
for (const [key, filterFn] of Object.entries(this.#filters)) {
result = result.filter(filterFn);
}
// Apply sort
if (this.#sortColumn) {
const col = this.#columns.find((c) => c.key === this.#sortColumn);
const compare = col?.sortFn || ((a, b) => {
const va = a[this.#sortColumn];
const vb = b[this.#sortColumn];
if (va < vb) return -1;
if (va > vb) return 1;
return 0;
});
result.sort((a, b) => {
const order = compare(a, b);
return this.#sortDirection === "asc" ? order : -order;
});
}
return result;
}
#getPageData() {
const processed = this.#getProcessedData();
const start = (this.#currentPage - 1) * this.#pageSize;
return {
rows: processed.slice(start, start + this.#pageSize),
total: processed.length,
totalPages: Math.ceil(processed.length / this.#pageSize),
};
}
render() {
const { rows, total, totalPages } = this.#getPageData();
this.#container.innerHTML = `
<table class="data-table">
<thead>
<tr role="row">
<th role="columnheader">
<input type="checkbox" class="select-all"
aria-label="Select all rows"
${this.#selectedRows.size === rows.length ? "checked" : ""}>
</th>
${this.#columns.map((col) => `
<th role="columnheader"
class="sortable ${this.#sortColumn === col.key ? `sorted-${this.#sortDirection}` : ""}"
data-column="${col.key}"
aria-sort="${this.#sortColumn === col.key ? this.#sortDirection + "ending" : "none"}">
${col.label}
<span class="sort-icon" aria-hidden="true"></span>
</th>
`).join("")}
</tr>
</thead>
<tbody>
${rows.map((row) => `
<tr role="row" data-id="${row.id}"
class="${this.#selectedRows.has(row.id) ? "selected" : ""}">
<td role="gridcell">
<input type="checkbox" class="select-row"
data-id="${row.id}"
${this.#selectedRows.has(row.id) ? "checked" : ""}
aria-label="Select row">
</td>
${this.#columns.map((col) => `
<td role="gridcell">${col.render ? col.render(row[col.key], row) : row[col.key]}</td>
`).join("")}
</tr>
`).join("")}
</tbody>
</table>
<div class="table-footer">
<span>${total} results</span>
<div class="pagination">
<button class="prev" ${this.#currentPage <= 1 ? "disabled" : ""} aria-label="Previous page">Prev</button>
<span>Page ${this.#currentPage} of ${totalPages}</span>
<button class="next" ${this.#currentPage >= totalPages ? "disabled" : ""} aria-label="Next page">Next</button>
</div>
</div>
`;
this.#bindEvents();
}
#bindEvents() {
// Sort
this.#container.querySelectorAll(".sortable").forEach((th) => {
th.addEventListener("click", () => {
const col = th.dataset.column;
if (this.#sortColumn === col) {
this.#sortDirection = this.#sortDirection === "asc" ? "desc" : "asc";
} else {
this.#sortColumn = col;
this.#sortDirection = "asc";
}
this.render();
});
});
// Pagination
this.#container.querySelector(".prev")?.addEventListener("click", () => {
this.#currentPage--;
this.render();
});
this.#container.querySelector(".next")?.addEventListener("click", () => {
this.#currentPage++;
this.render();
});
// Row selection
this.#container.querySelector(".select-all")?.addEventListener("change", (e) => {
const rows = this.#getPageData().rows;
if (e.target.checked) {
rows.forEach((r) => this.#selectedRows.add(r.id));
} else {
rows.forEach((r) => this.#selectedRows.delete(r.id));
}
this.render();
});
this.#container.querySelectorAll(".select-row").forEach((cb) => {
cb.addEventListener("change", (e) => {
const id = parseInt(e.target.dataset.id, 10);
if (e.target.checked) this.#selectedRows.add(id);
else this.#selectedRows.delete(id);
this.render();
});
});
}
addFilter(name, filterFn) {
this.#filters[name] = filterFn;
this.#currentPage = 1;
this.render();
}
removeFilter(name) {
delete this.#filters[name];
this.render();
}
getSelected() {
return [...this.#selectedRows];
}
}
// Usage
const table = new DataTable(document.querySelector("#table"), [
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "role", label: "Role", render: (v) => `<span class="badge badge-${v}">${v}</span>` },
{ key: "status", label: "Status", render: (v) => v ? "Active" : "Inactive" },
]);
table.setData([
{ id: 1, name: "Alice", email: "alice@example.com", role: "admin", status: true },
{ id: 2, name: "Bob", email: "bob@example.com", role: "user", status: true },
]);| Component | Accessibility Features | ARIA Roles |
|---|---|---|
| Dialog | Focus trap, Escape close, screen reader announcements | dialog, aria-modal |
| Form Field | Error announcements, required indicators, describedby | alert, aria-invalid |
| Data Table | Sort announcements, row selection, keyboard navigation | grid, columnheader, gridcell |
| Navigation | Current page indicator, landmark regions | navigation, aria-current |
| Notification | Live region announcements, auto-dismiss | alert, aria-live |
Rune AI
Key Insights
- Design token systems use CSS custom properties for runtime theming: Define tokens as variables on the document root and swap themes by updating those properties without re-rendering components
- Accessible components require focus management, ARIA attributes, and live regions: Every interactive component needs keyboard support, screen reader announcements, and proper semantic roles
- Complex form fields track touched, dirty, and validation state independently: Each field validates on blur and subsequent input, displaying inline errors with ARIA error associations
- Data tables combine sorting, pagination, filtering, and row selection: A processed data pipeline applies filters, then sorts, then paginates, re-rendering the table on each interaction
- Component composition through mounting and unmounting manages lifecycle: Parent components add and remove children with proper cleanup, preventing memory leaks and stale event listeners
Frequently Asked Questions
How do I handle design token changes at runtime?
How do I build accessible keyboard navigation for complex components?
Should I use Web Components or plain classes for enterprise UI?
How do I test UI components without a framework?
Conclusion
Enterprise UI systems in vanilla JavaScript require design tokens for consistent styling, accessible component bases for inclusive experiences, complex form architectures for data collection, and data table engines for structured information display. For the state management patterns that power these systems, see Vanilla JS State Management for Advanced Apps. For architectural patterns behind component communication, explore Building an Event Bus with JS Pub/Sub Pattern.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.