Educational Web Components Demo

Safari and Built-In Custom Elements

A practical compatibility walkthrough based on Zhang Xinxu's article: Safari supports autonomous custom elements, but not customized built-in elements. These demos focus on the implementation ideas without reproducing the article.

1. Autonomous Elements Work Everywhere Custom Elements Are Supported

Autonomous custom elements use their own tag names, such as <ui-tips>. Safari supports this category.

Code:
class UiTips extends HTMLElement {
  connectedCallback() {
    this.textContent = 'Autonomous custom element upgraded successfully.';
  }
}

customElements.define('ui-tips', UiTips);
Result:

2. Customized Built-In Elements Extend Native HTML

The elegant form is <button is="ui-action">, <input is="ui-color">, or similar. Safari needs compatibility handling for this pattern.

Code:
class UiAction extends HTMLButtonElement {
  connectedCallback() {
    this.addEventListener('click', () => {
      this.textContent = 'Native button behavior preserved';
    });
  }
}

customElements.define('ui-action', UiAction, {
  extends: 'button'
});
Result:
Checking support...

If this browser does not support customized built-ins, the support indicator will show that the button was not upgraded.

3. Detect the Feature, Not the Browser

Instead of asking whether the browser is Safari, create a tiny customized built-in element and check whether it upgrades.

Code:
class AnyClass extends HTMLBRElement {
  constructor() {
    super();
    this.someMethod = true;
  }
}

if (!customElements.get('any-class-demo')) {
  customElements.define('any-class-demo', AnyClass, {
    extends: 'br'
  });
}

const isSupportBuiltIn = document.createElement('br', {
  is: 'any-class-demo'
}).someMethod;
Result:
Running detection... The page is creating a customized <br> element and reading its property.

4. Use a Compatibility Branch When Support Is Missing

A real project can load a polyfill. This self-contained demo uses the same feature flag to switch between native behavior and an explanatory fallback.

Code:
const supportMessage = isSupportBuiltIn
  ? 'Use customized built-in elements directly.'
  : 'Load a built-in custom elements polyfill before components.';

document.querySelector('#compatMessage').textContent = supportMessage;
Result:

Current branch

Checking support...

Why this matters

Business code can keep native tags while compatibility code handles unsupported browsers.

5. Wait for a Connected Event Before Calling Component Methods

Polyfilled customized built-ins can initialize later than native ones. Dispatching a custom event from connectedCallback gives business code a reliable hook.

Code:
class ReadyPanel extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<strong>ReadyPanel</strong><p>Waiting for method call.</p>';
    this.dispatchEvent(new CustomEvent('connected', {
      detail: { type: 'ready-panel' }
    }));
  }

  someMethod() {
    this.querySelector('p').textContent =
      'someMethod() ran after initialization.';
  }
}

customElements.define('ready-panel', ReadyPanel);

document.querySelector('ready-panel')
  .addEventListener('connected', function() {
    this.someMethod();
  });
Result: