Skip to main content
Version: v4.3.1

Creating Custom Elements

When building applications with webbloqs, you may need to create your own custom web components that wrap React components. This is useful when you need:

  • Server-side rendering via Lit SSR
  • Form integration via Form-Associated Custom Elements (FACE)
  • Framework-agnostic distribution as standard web components
  • Shadow DOM encapsulation for style isolation

Webbloqs provides this through @webbloqs/proxies, which bridges React components to custom elements.

Three-Layer Architecture​

Consumer projects use a three-layer pattern to create custom elements from React components:

LayerFileResponsibility
1. React ComponentMyComponent.tsxPure React — uses webbloqs components from @webbloqs/react
2. CustomElementMyComponentElement.tsxWraps the React component with CustomElement() factory from @webbloqs/proxies
3. React Wrapperreact.tsxRegisters the custom element and re-exports it as a typed React component via createComponent() — so consumers can use it in JSX with full prop types and IDE autocompletion

Each layer has a distinct responsibility. Let's walk through a real example.

Step-by-step Example​

Layer 1: React Component​

Write a standard React component using webbloqs components. This is pure React — no web component concerns here.

// MyComponent.tsx
import { Button } from "@webbloqs/react/lib/button";
import { Stack } from "@webbloqs/react/lib/stack";
import { TextField } from "@webbloqs/react/lib/text-field";

export interface MyComponentProps {
label: string;
onSubmit: () => void;
}

export const MyComponent = ({ label, onSubmit }: MyComponentProps) => (
<Stack space="space--l">
<TextField name="email" label={label} type="email" required />
<Button kind="primary" onClick={onSubmit}>Submit</Button>
</Stack>
);

Layer 2: CustomElement Definition​

Wrap the React component with the CustomElement factory from @webbloqs/proxies. This defines the web component's properties, stylesheets, and optional form association.

// MyComponentElement.tsx
import { CustomElement } from "@webbloqs/proxies";
import { MyComponent, MyComponentProps } from "./MyComponent";
import Styles from "./my-component.scss";

export const MyComponentElement = CustomElement<MyComponentProps>({
reactComponent: MyComponent,
stylesheets: [Styles],
properties: {
label: String,
onSubmit: Function,
},
formAssociated: true, // Optional: enables form participation
});

The properties object maps component props to their types. The CustomElement factory handles:

  • Property/attribute synchronization
  • Shadow DOM rendering
  • Stylesheet adoption
  • SSR support via Lit SSR

For the full CustomElement API, see the @webbloqs/proxies documentation.

Layer 3: React Wrapper​

Register the custom element and create a React wrapper using createComponent. The registration guard prevents errors when the same element is registered twice.

// react.tsx
import React from "react";
import { createComponent } from "@webbloqs/react/lib/create-component";
import { MyComponentElement } from "./MyComponentElement";

if (customElements.get("my-org-my-component") === undefined) {
customElements.define("my-org-my-component", MyComponentElement);
}

export const MyComponent = createComponent({
react: React,
tagName: "my-org-my-component",
elementClass: MyComponentElement,
});

This React wrapper can now be imported and used like any React component, while the underlying implementation is a web component with shadow DOM.

Form Integration​

Setting formAssociated: true in the CustomElement definition enables Form-Associated Custom Elements (FACE). This lets your component participate in HTML forms — it can submit values, integrate with validation, and respond to form resets.

export const MyFieldElement = CustomElement<MyFieldProps>({
reactComponent: MyField,
stylesheets: [Styles],
properties: {
name: String,
value: String,
},
formAssociated: true,
});

For details on form association, see the Proxies FACE guide.

SSR Support​

Custom elements created with @webbloqs/proxies support server-side rendering through Lit SSR. The CustomElement factory handles the SSR lifecycle automatically — no additional configuration is needed in the element definition.

For SSR setup in your application, see the @webbloqs/proxies documentation.

Import Conventions​

Always import from subpaths for tree-shaking:

// Correct
import { Button } from "@webbloqs/react/lib/button";
import { CustomElement } from "@webbloqs/proxies";

// ⚠️ Avoid — imports the entire library
import { Button } from "@webbloqs/react";

Why subpath imports matter: Barrel imports bypass tree-shaking, pulling in all components. Some components have side effects that can break other functionality — particularly in SSR contexts. Use subpath imports until tree-shaking is fully addressed in webbloqs.

File Organization​

A typical component folder in a consumer project:

src/components/my-component/
├── MyComponent.tsx # Layer 1: React component
├── MyComponentElement.tsx # Layer 2: CustomElement definition
├── react.tsx # Layer 3: React wrapper + registration
├── my-component.scss # Shadow DOM styles
└── index.ts # Public exports