Skip to main content

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