Get JSX to recognise your custom element in React or Preact

Joel Malone
3 min readOct 10, 2023

--

King’s Skink by ka.hi on Flickr.

The problem

You want to use your custom elements in your React or Preact Typescript project, but the compiler won’t accept your custom elements:

The solution

Use Typescript’s Module Augmentation feature to add your custom element to JSX’s table of elements.

Read on for details.

Defining your <custom-element>

Let’s say you have a custom element, <custom-element>, and you want to use it in your React or Preact code.

Here’s an example <custom-element> implementation (as per the Custom Elements API):

// custom-element.ts

class CustomElement extends HTMLElement {
constructor() {
super();

const shadow = this.attachShadow({ mode: "open" });

// etc
}
}

customElements.define("custom-element", CustomElement);

You then use it in your React or Preact code, like this:

// my-app.tsx

import "./custom-element.ts";

export function MyApp() {
return <custom-element />;
}

But, the JSX interpreter (the thing that allows you to type HTML directly into your code), has no idea that the element named <custom-element> is in fact a reference to your CustomElement class, and produces this error:

The error does give you a hint:

Property ‘custom-element’ does not exist on type ‘JSX.IntrinsicElements’.

If we dig into node_modules and inspect that type, it appears to be an interface defining element names and their Javascript implementation:

So it makes sense that we probably need to add our <custom-element> to that interface. But, this particular interface is imported into our code, meaning we can’t edit it… right?

Right? RIGHT?

Declaration Merging

Using Typescript’s Declaration Merging feature, we can augment the imported JSX.IntrinsicElements interface to add an entry for our <custom-element>.

To augment the JSX.IntrinsicElements interface, put the following declare module ... code into the same file as your custom element’s, e.g.:

// custom-element.ts

// Note: if using Preact, change the module name to "preact/jsx-runtime"

declare module "react/jsx-runtime" {
namespace JSX {
interface IntrinsicElements {
"custom-element": JSX.HTMLAttributes<CustomElement>;
}
}
}

class CustomElement extends HTMLElement {
// ...

Now, elsewhere in your React or Preact code, you can do this:

export function MyApp() {
return <custom-element />;
}

See here for more details on Typescript’s Module Augmentation feature:

Side note: in your project, where does JSX live?

The examples above assume the JSX.IntrinsicElements interface lives at a particular module location, but this may change between versions of React and Preact, or any other JSX-based library.

In my case, I was using Preact, which has JSX embedded into itself and is exposed as a module named preact/jsx-runtime, but it took some trial-and-error to discover this.

When I tested the same in React, it the equivalent module name was react/jsx-runtime.

So, you may need to do some spelunking to discover the precise location of the module to be augmented. This will depend on exactly how JSX finds it’s way into your project — you probably don’t import it directly, but rather through React or Preact or some other library — so start digging there.

--

--

Joel Malone

Software Engineer living in Southwest Western Australia