template, and wires up attribute observation automatically.
Step 1: Define the Component Configuration
The configuration object requires a name (must include a hyphen), an array of props (attributes to observe), a template string, and optional hooks.
const metricCardConfig = {
name: 'metric-card',
props: ['value', 'label', 'trend'],
css: `
.card { padding: 1rem; border: 1px solid #e2e8f0; border-radius: 0.5rem; font-family: system-ui; }
.value { font-size: 1.5rem; font-weight: 600; }
.trend-up { color: #16a34a; }
.trend-down { color: #dc2626; }
`,
template: `
<div class="card">
<span class="label">{{label}}</span>
<div class="value">{{value}}</div>
<span class="trend {{trend}}">{{trend === 'up' ? 'â˛' : 'âź'}}</span>
</div>
`,
init: function(config) {
const host = this.getComp();
// Initial setup logic runs once on creation
host.setAttribute('role', 'region');
host.setAttribute('aria-label', `Metric: ${config.props.label}`);
},
watch: function(attrChange) {
if (attrChange.name === 'trend') {
const host = attrChange.comp;
const trendEl = host.querySelector('.trend');
trendEl.className = `trend trend-${attrChange.newValue}`;
}
}
};
Step 2: Register the Component
Registration is a single function call. The library handles customElements.define() under the hood.
import Drow from 'drow';
Drow.register(metricCardConfig);
Step 3: Use in Markup
Once registered, the component behaves like any native element. Attributes map directly to the props array.
<metric-card value="1,240" label="Active Users" trend="up"></metric-card>
Architecture Rationale
- Object over Class: Plain objects are serializable, easier to unit test, and align with configuration-driven patterns. They eliminate the need to manage
this binding across lifecycle methods.
- Automatic Shadow DOM: The library attaches an open shadow root automatically. This guarantees style encapsulation without manual
attachShadow() calls.
- Handlebars-style Interpolation: Template variables use
{{prop}} syntax. This is a simple string replacement mechanism, not a reactive binding engine. It keeps the parser lightweight and predictable.
init and watch Hooks: These replace connectedCallback and attributeChangedCallback. init runs once on instantiation, ideal for event listeners and ARIA setup. watch triggers on attribute changes, providing a clean interface for reacting to external state updates.
Pitfall Guide
Even with a streamlined API, production usage requires discipline. Here are the most common failure modes and how to avoid them.
-
Ignoring the Hyphen Requirement
Explanation: The Custom Elements spec mandates that all custom element names contain a hyphen (e.g., my-element). Drow will fail silently or throw during registration if this rule is violated.
Fix: Always prefix component names with a namespace or project identifier (e.g., app-metric, ui-badge).
-
Misunderstanding this Context in Hooks
Explanation: Inside init and watch, this refers to the libraryâs internal wrapper, not the DOM element itself. Attempting to call this.querySelector() will fail.
Fix: Always use this.getComp() in init or attrChange.comp in watch to access the actual host element.
-
Assuming Automatic Two-Way Binding
Explanation: The {{prop}} syntax performs one-time string interpolation during initial render. Changing a prop via JavaScript wonât automatically update the DOM unless you manually trigger a re-render or use the watch hook.
Fix: For dynamic updates, explicitly query the DOM in the watch hook or dispatch a custom event to a parent controller that manages state.
-
CSS Specificity Leaks
Explanation: While Shadow DOM provides encapsulation, inline styles or highly specific selectors in the css field can still cause rendering inconsistencies if not scoped properly.
Fix: Use class-based selectors exclusively. Avoid element selectors (e.g., div, span) in the css field to prevent unintended overrides.
-
Overcomplicating the watch Hook
Explanation: Developers often try to handle complex state transitions or multiple attribute changes within a single watch function, leading to tangled conditional logic.
Fix: Keep watch focused on DOM updates. For complex state, use a centralized store or dispatch CustomEvent instances to communicate changes upward.
-
Forgetting Attribute Serialization
Explanation: HTML attributes are always strings. Passing numbers or booleans directly (e.g., count="5") requires manual parsing in init or watch.
Fix: Always parse incoming attributes explicitly: const count = parseInt(this.getAttribute('count'), 10) || 0;
-
Event Listener Memory Leaks
Explanation: Attaching listeners in init without cleanup can cause memory leaks if components are dynamically added and removed from the DOM.
Fix: Store listener references and remove them when the component is disconnected. Since Drow doesnât expose a disconnectedCallback equivalent by default, manage cleanup at the application level or use AbortController for scoped event handling.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Marketing site with interactive widgets | Drow | Minimal overhead, fast load times, no build step required | Negligible |
| Enterprise design system with complex state | Lit | Built-in reactivity, decorator syntax, mature ecosystem | Moderate (~5kb + tooling) |
| Legacy app migration to web standards | Native Web Components | Zero dependencies, full spec control, gradual adoption | High (development time) |
| Rapid prototyping / internal tools | Drow | Object config maps directly to JSON/data sources, instant iteration | Negligible |
| High-frequency real-time dashboards | Lit or Svelte | Built-in reactive updates outperform manual DOM manipulation | Moderate to High |
Configuration Template
A production-ready component structure with error handling, type safety, and accessibility considerations.
const statusIndicatorConfig = {
name: 'status-indicator',
props: ['state', 'message'],
css: `
.wrapper { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.875rem; }
.dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; }
.state-active .dot { background-color: #22c55e; }
.state-inactive .dot { background-color: #94a3b8; }
.state-error .dot { background-color: #ef4444; }
`,
template: `
<div class="wrapper state-{{state}}">
<span class="dot" aria-hidden="true"></span>
<span class="label">{{message}}</span>
</div>
`,
init: function(config) {
const host = this.getComp();
const currentState = this.getAttribute('state') || 'inactive';
const currentMsg = this.getAttribute('message') || 'Unknown';
host.setAttribute('role', 'status');
host.setAttribute('aria-live', 'polite');
// Cache DOM references for performance
host._cachedLabel = host.querySelector('.label');
host._cachedWrapper = host.querySelector('.wrapper');
},
watch: function(attrChange) {
const host = attrChange.comp;
if (attrChange.name === 'state') {
const wrapper = host._cachedWrapper;
wrapper.className = `wrapper state-${attrChange.newValue}`;
}
if (attrChange.name === 'message') {
host._cachedLabel.textContent = attrChange.newValue;
}
}
};
Quick Start Guide
- Install the package: Run
npm install drow or include the minified script via a <script> tag in your HTML.
- Define your configuration: Create a plain object with
name, props, template, and optional css, init, and watch fields.
- Register the component: Call
Drow.register(yourConfig) after the DOM is ready or inside your module entry point.
- Drop it into markup: Use the custom element tag in your HTML, passing attributes that match your
props array.
- Verify interactivity: Open DevTools, inspect the element, and confirm the shadow DOM is attached and styles are scoped correctly.
This approach strips away the friction of native Web Components while preserving their core advantages: framework independence, style encapsulation, and native browser support. By treating components as configuration objects rather than class instances, you gain a predictable, testable, and extremely lightweight development model that scales cleanly from single-page widgets to modular interface libraries.