Web Components Red Flags and Green Flags

2026-03-29SPUNK13spunk.bet

Custom elements have been shippable for years, and the difference between a component library people adopt and one they route around comes down to a handful of implementation choices. Here is how to tell which one you are looking at within about two minutes of reading the source.

Red Flags

Shadow root opened with mode: 'closed' — it blocks your own test tooling and every debugging session, and it stops nobody determined. Use open unless you have a specific, written reason.

No styling API — if the only way to restyle a button is !important from outside, the shadow boundary has become a wall. Expose ::part() hooks and custom properties, or consumers will fork the component.

Listeners and observers added in connectedCallback with no matching disconnectedCallback — every mount leaks a ResizeObserver or a document-level listener. This is the single most common memory leak in component libraries.

Reaching outside itselfdocument.querySelector from inside a component couples it to a page structure it does not own. Take references via slots, attributes or properties instead.

innerHTML built from attribute values — an attribute is untrusted input the moment anything renders user data into markup. Set textContent, or sanitise deliberately.

No server rendering story — a component that renders nothing until JavaScript executes turns every page into a blank box for the first second and hurts LCP directly.

Green Flags

Attributes and properties stay in syncstatic observedAttributes lists the reactive ones, attributeChangedCallback updates state, and property setters reflect back to attributes where it makes sense. Frameworks pass strings via attributes and objects via properties; a component that only handles one breaks in half of them.

Form-associated via ElementInternalsstatic formAssociated = true plus internals.setFormValue() and setValidity() means the component submits with the form, participates in :invalid, and works with native validation instead of reimplementing it.

Declarative Shadow DOM support — shipping <template shadowrootmode="open"> in the server response means the component renders before any script runs and hydrates in place.

Events are real eventsnew CustomEvent('change', {bubbles: true, composed: true, detail}) crosses the shadow boundary and behaves like the platform. Callback props on a custom element do not.

Slots for content, not string attributes — named slots let consumers pass real markup, keep it in the light DOM where their CSS reaches it, and preserve accessibility semantics.

Focus handled explicitlyattachShadow({mode:'open', delegatesFocus:true}) and a sensible tabindex so keyboard users can reach the interactive part inside.

The quick audit

Load the component with JavaScript disabled: do you see anything? Mount and unmount it a hundred times and take a heap snapshot: does retained size grow? Put it in a native <form> and submit: does its value appear? Tab into it: does focus land somewhere sensible? Four checks, and they separate a component you can build on from one you will end up replacing.

Keep Going

Free tools, guides, and resources across the SPUNK13 network.

Visit spunk.bet400+ Free Tools
Dev ToolsCasinoMemesAstrologyScam DBBacklinksEbooks