chemical-and-materials-engineering
Creating Custom Web Components for Engineering Applications
Table of Contents
Web components provide a standards-based approach to building reusable, encapsulated UI elements that are particularly valuable in engineering applications where consistency, modularity, and cross-platform compatibility are paramount. Unlike framework-specific components, web components work natively in modern browsers and can be integrated into any project—regardless of the underlying library or CMS. For engineering teams using Directus as a headless CMS to manage complex data models, custom web components offer a way to create tailored front-end interfaces that communicate seamlessly with the Directus API, extending functionality without locking into a single framework.
This article expands on the fundamentals of web components, explores their benefits for engineering workflows, provides detailed creation steps with code examples, and demonstrates how to combine them with Directus to build powerful, maintainable applications.
What Are Web Components?
Web components are a collection of browser APIs that enable developers to create custom HTML elements with their own encapsulated behavior and styling. They are built on four primary specifications, though one has been superseded:
- Custom Elements – Define new HTML elements with custom behavior.
- Shadow DOM – Encapsulate DOM and CSS to prevent style and script leaks.
- HTML Templates – Define reusable HTML fragments that are not rendered until instantiated.
- ES Modules – Replace the deprecated HTML Imports for loading and sharing component code.
These specifications together allow engineers to build self-contained modules that can be authored once and used across multiple projects, including engineering dashboards, simulation tools, and control panels.
Custom Elements
Custom Elements enable authors to create new HTML tags that extend the browser’s element set. They can be either autonomous (extending HTMLElement) or built-in (extending existing HTML elements like <button> or <div>). With lifecycle callbacks such as connectedCallback and attributeChangedCallback, developers have granular control over a component’s lifecycle.
Shadow DOM
The Shadow DOM provides scoped styling and markup isolation. Styles defined inside a shadow tree do not affect the main document, and vice versa. This is critical in engineering applications where multiple third-party widgets may be combined on a single dashboard without CSS conflicts. The shadow root can be attached in open mode (accessible via element.shadowRoot) or closed mode (not accessible externally). For most engineering uses, open mode offers the best balance of encapsulation and testability.
HTML Templates
The <template> element holds HTML that is parsed but not rendered until its content is cloned and appended to the DOM. This is ideal for web components because it avoids re-parsing markup each time an instance is created, improving performance in data-heavy applications.
ES Modules
ES modules (<script type="module">) replace the deprecated HTML Imports for loading component dependencies. They enable clear import/export semantics, tree-shaking, and compatibility with modern build tools. Using ES modules, each web component can be authored as a single file and imported only when needed.
Why Web Components for Engineering Applications?
Engineering applications often involve complex interactive UIs—real-time data visualizations, parameter entry forms, simulation controls—that must work reliably across different teams, browsers, and even frameworks. Web components address these needs directly.
- Reusability Across Projects: A slider component for controlling simulation parameters can be developed once and reused in a fluid dynamics app, a structural analysis tool, and a monitoring dashboard, reducing duplication.
- Encapsulation: Isolated styling ensures that a custom gauge component does not accidentally inherit or override CSS from a parent application, a common problem in large engineering systems.
- Interoperability: Web components can be used with React, Vue, Angular, or plain JavaScript. This allows different engineering teams to adopt their preferred framework while sharing a common component library.
- Future-Proofing: Because web components rely on browser standards, they are not tied to the release cycle of a particular framework. Components written today will likely work seamlessly for years.
- Performance: Without virtual DOM overhead and with minimal runtime, web components can be highly performant, especially when rendering many instances of the same element—common in grid-based engineering visualizations.
- Maintainability: Each component is a self-contained module with its own tests, documentation, and versioning. Teams can update individual components without affecting the rest of the system.
How to Create Custom Web Components
Building a web component involves several steps. Below is a detailed walkthrough, expanding on the original example to include lifecycle callbacks, attribute observation, and styling best practices.
1. Define the Custom Element Class
Start by creating a JavaScript class that extends HTMLElement (for autonomous elements). For engineering applications, the class should include at least the constructor and may implement lifecycle callbacks:
constructor()– Set up initial state, attach shadow DOM, and parse any initial attributes.connectedCallback()– Called when the element is inserted into the DOM. Use this for setup like fetching data or adding event listeners.disconnectedCallback()– Clean up timers, subscriptions, or removed DOM nodes.attributeChangedCallback(name, oldValue, newValue)– React to changes in observed attributes.adoptedCallback()– Invoked when the element is moved into a new document.
2. Attach Shadow DOM
Inside the constructor, call this.attachShadow({ mode: 'open' }) to create a shadow tree. The shadow root becomes the container for all internal markup and styles.
class EngineeringButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// ... rest of setup
}
}
3. Define Template and Styles
Use a <template> element to define the HTML structure and CSS. This markup is cloned each time a new instance is created, which is more efficient than setting innerHTML in the constructor for each element.
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: inline-block;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
button {
background-color: #007acc;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background-color: #005a9e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<button><slot>Button</slot></button>
`;
Notice the use of :host to style the custom element itself, and <slot> to allow users to insert custom content (like an icon or label) inside the button.
4. Register the Element
Finally, register the new element with the browser via customElements.define(). The element name must contain a hyphen (e.g., engineering-button).
customElements.define('engineering-button', EngineeringButton);
5. Add Attributes and Observers
For an engineering button that can control a simulation parameter, you might want attributes like value or disabled. Use static get observedAttributes() to specify which attributes to watch.
class EngineeringButton extends HTMLElement {
static get observedAttributes() {
return ['disabled', 'variant'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
this.shadowRoot.querySelector('button').disabled = newValue !== null;
}
if (name === 'variant') {
this.updateVariant(newValue);
}
}
updateVariant(variant) {
const button = this.shadowRoot.querySelector('button');
button.style.backgroundColor = variant === 'secondary' ? '#6c757d' : '#007acc';
}
}
Full Example: Engineering Button with Attributes
Combining all steps, here is a complete web component for a reusable button used in an engineering tool interface:
const template = document.createElement('template');
template.innerHTML = `
<style>
:host { display: inline-block; }
button {
padding: 0.5rem 1.25rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
button:focus { outline: 2px solid #005a9e; outline-offset: 2px; }
</style>
<button><slot>Click</slot></button>
`;
class EngineeringButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
static get observedAttributes() {
return ['disabled', 'variant'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
this.shadowRoot.querySelector('button').disabled = newValue !== null;
}
if (name === 'variant') {
const button = this.shadowRoot.querySelector('button');
const variants = {
primary: '#007acc',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545'
};
button.style.backgroundColor = variants[newValue] || '#007acc';
}
}
}
customElements.define('engineering-button', EngineeringButton);
Use it in HTML:
<engineering-button variant="primary">Simulate</engineering-button>
<engineering-button variant="danger" disabled>Abort Mission</engineering-button>
Advanced Patterns for Engineering Applications
State Management and Data Binding
For components that handle real-time engineering data (e.g., sensor values, simulation results), manage state internally using JavaScript properties. Trigger re-renders by updating the shadow DOM directly. Consider using custom events to communicate changes to parent applications.
class SensorGauge extends HTMLElement {
set value(val) {
this._value = val;
this.shadowRoot.querySelector('.gauge-fill').style.width = `${val}%`;
this.dispatchEvent(new CustomEvent('value-changed', { detail: val }));
}
get value() {
return this._value;
}
}
Integration with Frameworks
Web components integrate naturally with modern frameworks, but some require wrappers for full reactivity. For React, use ref and set properties directly; for Vue, use v-bind and v-on (which work out of the box with custom events). Angular provides CUSTOM_ELEMENTS_SCHEMA to avoid strict schema errors.
For Directus App extensions built with Vue, web components can be used as custom interface or display components by wrapping them in a Vue component that handles props down and events up.
Real-World Engineering Use Cases
- Data Visualization Widgets: Reusable charts, gauges, and heat maps that can be dropped into different dashboards without worrying about library conflicts.
- Simulation Control Panels: Sliders, numeric input fields with validation, and start/stop buttons that encapsulate the UI logic for a particular solver.
- CAD Viewer Integration: Custom elements that display 3D models using Three.js or WebGL, with attributes to load different model files.
- IoT Dashboard Components: Components that poll sensor data from WebSocket or REST APIs and update their display in real time.
- Field Interface for Headless CMS: Custom input elements for Directus that parse and validate engineering-specific data types, such as coordinates or material properties.
Integrating Web Components with Directus
Directus is a headless CMS that exposes your engineering data schema via a REST or GraphQL API. Custom web components can be used both on the front end (customer portals, monitoring apps) and as Directus extensions (custom interfaces or displays) that run inside the Directus App.
For example, you might need a custom input for entering and validating 3D vector coordinates (x, y, z). A web component can encapsulate three numeric inputs, ensure proper formatting, and emit the result as a JSON object. This component can then be used as a Directus Interface extension, providing a rich editing experience directly inside the CMS.
Here is a conceptual example of a Directus custom interface that uses a web component:
// extension/src/index.js
import '../components/vector-input.js';
export default {
id: 'vector-input',
name: 'Vector Input',
icon: 'my_location',
component: {
inject: ['values'],
props: {
value: {
type: Object,
default: () => ({ x: 0, y: 0, z: 0 })
}
},
template: `
<vector-input
:value="value"
@input="update"
/>
`,
methods: {
update(e) {
this.$emit('input', e.detail);
}
}
}
};
This bridge component passes data between Vue (the Directus App) and the web component. To learn more, refer to the Directus Interface Extensions documentation.
Best Practices for Engineering Web Components
Naming Conventions
Always use a hyphen in the element name. Avoid generic names like <slider>; prefer <eng-slider> or <thermo-gauge> to prevent conflicts.
Accessibility
Ensure components are accessible by using ARIA roles, managing focus, and providing keyboard navigation. Test with screen readers, especially for interactive controls like sliders and buttons.
<button role="switch" aria-checked="false" ...></button>
Testing
Unit test web components using tools like Web Test Runner or Jest with @open-wc/testing. Test lifecycle hooks, attribute changes, and event dispatching.
Performance
For lists with many instances (e.g., a grid of sensor indicators), avoid calling document.createElement repeatedly; instead, clone templates. Consider using constructable style sheets for shared CSS across components to improve memory use.
Styling with CSS Custom Properties
Allow theme customization by exposing CSS custom properties. Set fallback values inside the component and let consumers override them from outside.
:host {
--button-bg: #007acc;
}
button {
background-color: var(--button-bg, #007acc);
}
Conclusion
Custom web components offer engineering teams a robust, standards-driven way to build reusable UI elements that work across frameworks and persist through technology changes. Their encapsulation and modularity simplify the development of complex interfaces, from real-time dashboards to headless CMS field editors. By pairing web components with a flexible backend like Directus, engineers can create consistent, maintainable, and scalable applications that serve both internal workflows and external user interfaces. Start small with a single custom button or gauge, and expand your component library as your engineering needs evolve.
For further reading, consult the MDN Web Components documentation and the webcomponents.org community.