Upgrade and Timing
What “upgrade” means
Section titled “What “upgrade” means”When the browser parses HTML it may encounter a tag like <my-el> before any JavaScript has run. At that point the browser has no registered constructor for my-el, so it creates the element as a plain HTMLElement — an unknown element with no special behaviour.
Later, when your script calls customElements.define('my-el', MyEl), the browser performs an upgrade: it walks every matching element already in the document, calls the MyEl constructor on each one, and then calls connectedCallback if the element is currently attached to the DOM.
This means upgrade can happen in two different orders:
- Define before parse — your
<script>in<head>(withdefer) runs before the parser reaches<my-el>. The element is constructed immediately when the parser reaches the tag. - Define after parse — a lazily loaded script runs after the HTML is already in the DOM. Every
<my-el>already in the document is upgraded at the momentdefineis called.
Both orders are intentional and supported. What you must not do is assume the element’s custom behaviour is available before define has been called.
Waiting with customElements.whenDefined
Section titled “Waiting with customElements.whenDefined”customElements.whenDefined(tagName) returns a Promise that resolves to the constructor class as soon as that tag name is registered. Use it to defer any code that depends on the element’s upgraded interface:
// Promise / .then() stylecustomElements.whenDefined('my-el').then(function(Ctor) { console.log('my-el is now defined:', Ctor); document.querySelector('my-el').doSomething();});// async / await styleasync function init() { const Ctor = await customElements.whenDefined('my-el'); console.log('my-el is now defined:', Ctor); document.querySelector('my-el').doSomething();}init();If the element is already defined when you call whenDefined, the promise resolves immediately on the next microtask tick.
The :defined CSS pseudo-class
Section titled “The :defined CSS pseudo-class”Before upgrade, a custom element matches :not(:defined) in CSS. After customElements.define is called and the upgrade runs, the element matches :defined. This gives you a clean hook to hide partially-rendered elements until they are ready — preventing a Flash of Undefined Content (FOUC):
my-el:not(:defined) { visibility: hidden;}A more complete FOUC-prevention pattern that also shows a loading placeholder:
<style> upgrade-demo:not(:defined) { visibility: hidden; }
upgrade-demo:defined { display: block; border: 2px solid #059669; }</style>
<upgrade-demo>Loading…</upgrade-demo>/* Optionally animate the reveal */upgrade-demo { transition: opacity 0.3s ease;}upgrade-demo:not(:defined) { opacity: 0;}upgrade-demo:defined { opacity: 1;}Ordering caveats
Section titled “Ordering caveats”The timing of define relative to DOM parsing matters:
- Script in
<head>withoutdefer— the script blocks parsing, sodefineis called before any body elements are parsed. Elements are constructed immediately when the parser reaches them. - Script in
<head>withdefer— the script runs after the full document is parsed. Every element already in the DOM is upgraded at once whendefineis called. - Dynamically imported or lazily loaded scripts — same as
defer: all existing elements upgrade in a batch atdefinetime. - Elements added via JavaScript after
define— these are upgraded synchronously as they are inserted into the document.
Always use customElements.whenDefined (or customElements.upgrade(element) for a single node) when you need to guarantee the element is ready before you access its custom API.
Runnable demo
Section titled “Runnable demo”Watch the element start as a plain placeholder, then flip to its upgraded state after 1.2 seconds when customElements.define fires inside the setTimeout.