react
Show how to use custom elements in React, including workarounds for React 18 vs 19 differences. Use when the user asks to "use in React", "React wrapper", "custom elements in React", "JSX custom element", or mentions "React 18", "React 19", or "react integration".
What this skill does
# Custom Elements in React
Generate framework-specific guidance for using custom elements in React, adapting to the project's React version.
## Workflow
### Phase 1: Detect React Version
Search the project for React version:
```bash
# Check package.json for react version
grep -A1 '"react"' package.json
```
Determine if the project uses:
- **React 19+**: Native custom element support
- **React 18 or earlier**: Requires workarounds for properties and events
Also check for:
- TypeScript (need JSX type declarations)
- Existing wrapper patterns in the codebase
- Whether `@lit/react` or similar wrapper libraries are already in use
### Phase 2: Gather Element Data
Read the target element(s):
```text
cem://element/{tagName}
cem://element/{tagName}/attributes
cem://element/{tagName}/events
```
Understand the element's API to know which features need framework-specific handling:
- **Attributes** (string-only): Work in all React versions
- **Properties** (complex types): Need workarounds in React 18
- **Events** (custom events): Need workarounds in React 18
- **Slots**: Map to `children` and named slot patterns in JSX
- **Boolean attributes**: Differ between React and HTML semantics
### Phase 3: Generate Integration Code
#### React 19+
React 19 supports custom elements natively. Properties are set correctly and custom events are handled.
```tsx
// Direct usage — no wrappers needed
function MyComponent() {
return (
<my-element
variant="primary"
complexData={someObject}
onMyEvent={(e) => handleEvent(e)}
>
<span slot="icon">★</span>
Button text
</my-element>
);
}
```
Key React 19 behaviors:
- Props matching an element property are set as properties, not attributes
- `on` + PascalCase event name maps to custom events (e.g., `onChange` -> `change`)
- `className` is set as the `class` attribute
- Boolean attributes work correctly
#### React 18 and Earlier
React 18 treats custom elements like unknown HTML elements — all props are set as attributes (stringified), and custom events are not supported via JSX.
**Option A: Use `@lit/react` wrappers (recommended)**
```tsx
import { createComponent } from '@lit/react';
import { MyElement } from 'my-element-library';
export const MyElementReact = createComponent({
tagName: 'my-element',
elementClass: MyElement,
react: React,
events: {
onMyEvent: 'my-event',
onChange: 'change',
},
});
// Usage
function MyComponent() {
return (
<MyElementReact
variant="primary"
complexData={someObject}
onMyEvent={(e) => handleEvent(e)}
>
Button text
</MyElementReact>
);
}
```
**Option B: Use refs for properties and events**
```tsx
function MyComponent() {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
// Set complex properties via ref
(el as any).complexData = someObject;
// Add event listeners
const handler = (e: Event) => handleEvent(e);
el.addEventListener('my-event', handler);
return () => el.removeEventListener('my-event', handler);
}, [someObject]);
return (
<my-element ref={ref} variant="primary">
Button text
</my-element>
);
}
```
**Option C: Use the CEM `export react` wrappers**
If the project uses CEM's export command, check for pre-generated React wrappers:
```bash
cem export react
```
This generates typed React wrapper components from the manifest.
#### TypeScript Declarations
For both React versions, add JSX type declarations so TypeScript knows about custom element attributes:
```tsx
// custom-elements.d.ts
import type { MyElement } from 'my-element-library';
declare global {
namespace JSX {
interface IntrinsicElements {
'my-element': React.DetailedHTMLProps<
React.HTMLAttributes<MyElement> & {
variant?: 'primary' | 'secondary';
disabled?: boolean;
// For React 18: only string attributes here
// For React 19: include property types too
},
MyElement
>;
}
}
}
```
For React 19, the declaration can include complex property types. For React 18, only include attributes (strings/booleans) since complex types won't pass through correctly without a wrapper.
#### Slot Mapping
Map named slots to JSX:
```tsx
// HTML slots -> JSX
<my-element>
{/* Default slot */}
<span>Default content</span>
{/* Named slots */}
<span slot="header">Header content</span>
<span slot="footer">Footer content</span>
</my-element>
```
### Phase 4: Output
Present the integration code with:
1. **Version-appropriate code**: Only show React 19 patterns if the project uses React 19+
2. **TypeScript declarations**: If the project uses TypeScript
3. **Event handling**: For each event in the manifest
4. **Property passing**: For each non-string property
5. **Import statements**: Show how to import the element registration
## React Version Reference
| Feature | React 18 | React 19+ |
|---------|----------|-----------|
| String attributes | Passed as attributes | Passed as attributes |
| Boolean attributes | `""` or `undefined` | Correct HTML behavior |
| Complex properties | Stringified (broken) | Set as properties |
| Custom events | Not supported in JSX | `onEventName` works |
| `className` | Set as attribute | Set as `class` |
| `ref` | Works | Works |
| SSR | Attributes only | Attributes only |
## Guidelines
- **Check the React version first**: The guidance differs significantly between 18 and 19
- **Prefer `@lit/react` wrappers for React 18**: They handle properties and events correctly with minimal boilerplate
- **Generate TypeScript declarations from the manifest**: The element's attributes and their types are all available
- **Don't suggest `dangerouslySetInnerHTML`**: Custom elements use slots, not innerHTML
- **Handle SSR**: Note that server-rendered custom elements only have access to attributes, not properties
Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.