Web Components

Preact's tiny size and standards-first approach make it a great choice for building web components.

Web Components are a set of standards that make it possible to build new HTML element types - Custom Elements like <material-card> or <tab-bar>. Preact fully supports these standards, allowing seamless use of Custom Element lifecycles, properties and events.

Preact is designed to render both full applications and individual parts of a page, making it a natural fit for building Web Components. Many companies use it to build component or design systems that are then wrapped up into a set of Web Components, enabling re-use across multiple projects and within other frameworks.

Preact and Web Components are complementary technologies: Web Components provide a set of low-level primitives for extending the browser, and Preact provides a high-level component model that can sit atop those primitives.



Rendering Web Components

In Preact, web components work just like other DOM Elements. They can be rendered using their registered tag name:

customElements.define('x-foo', class extends HTMLElement {
  // ...
});

function Foo() {
  return <x-foo />;
}
Run in REPL

Properties and Attributes

JSX does not provide a way to differentiate between properties and attributes. Custom Elements generally rely on custom properties in order to support setting complex values that can't be expressed as attributes. This works well in Preact, because the renderer automatically determines whether to set values using a property or attribute by inspecting the affected DOM element. When a Custom Element defines a setter for a given property, Preact detects its existence and will use the setter instead of an attribute.

customElements.define('context-menu', class extends HTMLElement {
  set position({ x, y }) {
    this.style.cssText = `left:${x}px; top:${y}px;`;
  }
});

function Foo() {
  return <context-menu position={{ x: 10, y: 20 }}> ... </context-menu>;
}
Run in REPL

When rendering static HTML using preact-render-to-string ("SSR"), complex property values like the object above are not automatically serialized. They are applied once the static HTML is hydrated on the client.

Accessing Instance Methods

To be able to access the instance of your custom web component, we can leverage refs:

function Foo() {
  const myRef = useRef(null);

  useEffect(() => {
    if (myRef.current) {
      myRef.current.doSomething();
    }
  }, []);

  return <x-foo ref={myRef} />;
}
Run in REPL

Triggering custom events

Preact normalizes the casing of standard built-in DOM Events, which are normally case-sensitive. This is the reason it's possible to pass an onChange prop to <input>, despite the actual event name being "change". Custom Elements often fire custom events as part of their public API, however there is no way to know what custom events might be fired. In order to ensure Custom Elements are seamlessly supported in Preact, unrecognized event handler props passed to a DOM Element are registered using their casing exactly as specified.

// Built-in DOM event: listens for a "click" event
<input onClick={() => console.log('click')} />

// Custom Element: listens for "TabChange" event (case-sensitive!)
<tab-bar onTabChange={() => console.log('tab change')} />

// Corrected: listens for "tabchange" event (lower-case)
<tab-bar ontabchange={() => console.log('tab change')} />
Run in REPL

Creating a Web Component

Any Preact component can be turned into a web component with preact-custom-element, a very thin wrapper that adheres to the Custom Elements v1 spec.

import register from 'preact-custom-element';

const Greeting = ({ name = 'World' }) => (
  <p>Hello, {name}!</p>
);

register(Greeting, 'x-greeting', ['name']);
//          ^            ^           ^
//          |      HTML tag name     |
//   Component definition      Observed attributes
Run in REPL

Note: As per the Custom Element Specification, the tag name must contain a hyphen (-).

Use the new tag name in HTML, attribute keys and values will be passed in as props:

<x-greeting name="Billy Jo"></x-greeting>

Output:

<p>Hello, Billy Jo!</p>

Observed Attributes

Web Components require explicitly listing the names of attributes you want to observe in order to respond when their values are changed. These can be specified via the third parameter that's passed to the register() function:

// Listen to changes to the `name` attribute
register(Greeting, 'x-greeting', ['name']);

If you omit the third parameter to register(), the list of attributes to observe can be specified using a static observedAttributes property on your Component. This also works for the Custom Element's name, which can be specified using a tagName static property:

import register from 'preact-custom-element';

// <x-greeting name="Bo"></x-greeting>
class Greeting extends Component {
  // Register as <x-greeting>:
  static tagName = 'x-greeting';

  // Track these attributes:
  static observedAttributes = ['name'];

  render({ name }) {
    return <p>Hello, {name}!</p>;
  }
}
register(Greeting);
Run in REPL

If no observedAttributes are specified, they will be inferred from the keys of propTypes if present on the Component:

// Other option: use PropTypes:
function FullName({ first, last }) {
  return <span>{first} {last}</span>
}

FullName.propTypes = {
  first: Object,   // you can use PropTypes, or this
  last: Object     // trick to define un-typed props.
};

register(FullName, 'full-name');
Run in REPL

Passing slots as props

The register() function has a fourth parameter to pass options. Currently only the shadow options is supported, which attaches a shadow DOM tree to the specified element. When enabled, this allows the use of named <slot> elements to forward the Custom Element's children to specific places in the shadow tree.

function TextSection({ heading, content }) {
    return (
        <div>
            <h1>{heading}</h1>
            <p>{content}</p>
        </div>
    );
}

registerElement(Foo, 'text-section', [], { shadow: true });
Run in REPL

Usage:

<text-section>
  <slot name="heading">Nice heading</slot>
  <slot name="content">Great content</slot>
</text-section>

Built by a bunch of lovely people like @valotas.