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:
| Layer | File | Responsibility |
|---|---|---|
| 1. React Component | MyComponent.tsx | Pure React — uses webbloqs components from @webbloqs/react |
| 2. CustomElement | MyComponentElement.tsx | Wraps the React component with CustomElement() factory from @webbloqs/proxies |
| 3. React Wrapper | react.tsx | Registers 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
CustomElementAPI, 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