@helixui/react — React Wrappers
apps/docs/src/content/docs/framework-integration/react-wrappers Click to copy apps/docs/src/content/docs/framework-integration/react-wrappers @helixui/react — React Wrappers
Section titled “@helixui/react — React Wrappers”@helixui/react provides auto-generated React wrapper components for every HELiX component. Each wrapper replaces ref-based addEventListener boilerplate with idiomatic React callback props (onHxClick, onHxChange, etc.) and ships full TypeScript types.
Why Use the Wrapper Package?
Section titled “Why Use the Wrapper Package?”React’s synthetic event system does not forward custom DOM events. Without wrappers, listening to an hx-click event requires this:
const ref = useRef<HTMLElement>(null);useEffect(() => { const el = ref.current; if (!el) return; el.addEventListener('hx-click', handleClick); return () => el.removeEventListener('hx-click', handleClick);}, [handleClick]);return <hx-button ref={ref}>Save</hx-button>;With @helixui/react, that becomes:
import { HxButton } from '@helixui/react';return <HxButton onHxClick={handleClick}>Save</HxButton>;The wrapper also provides:
- Typed props and events — each wrapper exposes the underlying CEM properties as typed React props plus typed
onHx*callbacks (no manualhelix.d.tsfor the attribute side) - Slot composition — child elements with
slot="<name>"are forwarded to the host; the slot names themselves are not encoded in the React prop type - Automatic ref forwarding —
refgives you the underlyingHTMLElement - React DevTools labels — component tree shows
<HxButton>instead of<hx-button>
Installation
Section titled “Installation”npm install @helixui/react@helixui/react has a peer dependency on react and react-dom. No additional setup is required — the package registers web components internally.
Import Patterns
Section titled “Import Patterns”Named imports (recommended)
Section titled “Named imports (recommended)”Import only what you need. Each component is tree-shakeable:
import { HxButton } from '@helixui/react';import { HxTextInput } from '@helixui/react';import { HxCard, HxDialog } from '@helixui/react';Barrel import
Section titled “Barrel import”Import everything from the package root. Convenient for prototyping; prefer named imports in production for smaller bundles:
import * as Helix from '@helixui/react';// <Helix.HxButton>, <Helix.HxTextInput>, ...Component naming convention
Section titled “Component naming convention”Every wrapper uses the PascalCase version of the tag name:
| Tag name | React wrapper |
|---|---|
hx-button | HxButton |
hx-text-input | HxTextInput |
hx-card | HxCard |
hx-dialog | HxDialog |
hx-badge | HxBadge |
Typed Event Callbacks
Section titled “Typed Event Callbacks”Every hx-* event exposed by a component is available as a typed on* prop. The event name is converted from kebab-case to camelCase with an on prefix:
| DOM event | React prop | Emitting components |
|---|---|---|
hx-click | onHxClick | hx-button, hx-card (when hx-href), etc. |
hx-input | onHxInput | hx-text-input, hx-textarea |
hx-change | onHxChange | hx-text-input, hx-select, hx-checkbox, etc. |
hx-open | onHxOpen | hx-dialog, hx-accordion-item, etc. |
hx-close | onHxClose | hx-dialog, hx-alert (after-close), etc. |
The wrapper exposes every event the underlying component declares in its CEM. Components do not emit hx-focus / hx-blur events — use native React onFocus / onBlur (or DOM focusin / focusout) for focus handling on hx-* elements.
Event handler types
Section titled “Event handler types”Generated onHx* props are typed as (event: Event) => void to match @lit-labs/react’s event-prop signature. Narrow the event inside the handler with a CustomEvent cast and your component’s detail shape (the detail payload is documented in each component’s CEM entry):
import { HxTextInput } from '@helixui/react';
function SearchField() { const handleInput = (event: Event) => { const { value } = (event as CustomEvent<{ value: string }>).detail; console.log(value); };
return ( <HxTextInput label="Search" placeholder="Type to search…" onHxInput={handleInput} /> );}Button click example
Section titled “Button click example”import { HxButton } from '@helixui/react';
function SaveButton({ onSave }: { onSave: () => void }) { return ( <HxButton variant="primary" onHxClick={onSave}> Save </HxButton> );}Ref Forwarding
Section titled “Ref Forwarding”Pass a ref to get the underlying HTMLElement. Use this to call imperative methods or read DOM properties directly:
import { useRef } from 'react';import { HxButton, HxDialog } from '@helixui/react';
function ConfirmDialog() { // hx-dialog exposes show() / showModal() / close() — there is no hide() method. const dialogRef = useRef<HTMLElement & { show: () => void; showModal: () => void; close: () => void; }>(null);
const open = () => dialogRef.current?.showModal(); const close = () => dialogRef.current?.close();
return ( <> <HxButton onHxClick={open}>Open dialog</HxButton> <HxDialog ref={dialogRef} heading="Confirm action" modal onHxClose={close}> <p>Are you sure?</p> <HxButton slot="footer" variant="primary" onHxClick={close}>Confirm</HxButton> <HxButton slot="footer" variant="ghost" onHxClick={close}>Cancel</HxButton> </HxDialog> </> );}Pass slot content using the slot attribute on child elements, exactly as you would with raw web components:
import { HxButton, HxCard } from '@helixui/react';
function PatientCard({ name, id }: { name: string; id: string }) { return ( <HxCard> <h3 slot="heading">{name}</h3> <p>Patient ID: {id}</p> <HxButton slot="footer" variant="ghost" hx-size="sm"> View record </HxButton> </HxCard> );}Tree-Shaking Verification
Section titled “Tree-Shaking Verification”To confirm only the components you import are included in your bundle, inspect with your bundler’s analysis tool:
# Next.js bundle analyzernpm install --save-dev @next/bundle-analyzer
# next.config.tsconst withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: true });export default withBundleAnalyzer({});
# RunANALYZE=true npm run buildIn the bundle map, you should see individual component chunks (e.g., hx-button.js, hx-text-input.js) rather than a single monolithic @helixui chunk.
Per-component sizes vary — small atoms sit under 5 KB min+gz; richer components (combobox, dialog, dropdowns) carry budgeted overages defined in bundle-budgets.json at the repo root. Run node scripts/measure-component-size.js (in the HELiX monorepo) or pnpm run check:bundle to see current numbers.
Next.js 15 App Router Integration
Section titled “Next.js 15 App Router Integration”The 'use client' requirement
Section titled “The 'use client' requirement”Custom elements register via customElements.define, which is a browser-only API. Any file importing from @helixui/react must be a Client Component:
'use client'; // Required — always at the top of files importing @helixui/react
import { HxButton } from '@helixui/react';
export function ActionButton({ label, onClick }: { label: string; onClick: () => void }) { return <HxButton variant="primary" onHxClick={onClick}>{label}</HxButton>;}Global registration in the root layout
Section titled “Global registration in the root layout”For apps that use HELiX components on many pages, register all components once in the root layout:
'use client';
import { useEffect } from 'react';
export function HelixProvider({ children }: { children: React.ReactNode }) { useEffect(() => { // Registration happens automatically when @helixui/react is imported. // This component exists to ensure it loads on the client. import('@helixui/react'); }, []);
return <>{children}</>;}// app/layout.tsx (Server Component — no 'use client' needed)import { HelixProvider } from '@/components/helix-provider';
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <HelixProvider>{children}</HelixProvider> </body> </html> );}Per-route Client Component pattern
Section titled “Per-route Client Component pattern”For interactive islands, wrap only the interactive parts in Client Components:
'use client';
import { useState } from 'react';import { HxTextInput, HxButton } from '@helixui/react';
interface PatientSearchProps { onSearch: (query: string) => void;}
export function PatientSearch({ onSearch }: PatientSearchProps) { const [query, setQuery] = useState('');
return ( <div className="search-bar"> <HxTextInput label="Search patients" placeholder="Name or patient ID" value={query} onHxInput={(e) => setQuery((e as CustomEvent<{ value: string }>).detail.value)} /> <HxButton variant="primary" disabled={!query} onHxClick={() => onSearch(query)} > Search </HxButton> </div> );}// app/patients/page.tsx (Server Component)import { PatientSearch } from '@/components/patient-search';
export default function PatientsPage() { return ( <main> <h1>Patient Records</h1> <PatientSearch onSearch={async (query) => { 'use server'; /* ... */ }} /> </main> );}Complete Example App
Section titled “Complete Example App”A minimal patient portal UI demonstrating common wrapper patterns:
// app/page.tsx (Server Component)import { PatientPortal } from '@/components/patient-portal';
export default function HomePage() { return <PatientPortal />;}'use client';
import { useState } from 'react';import { HxButton, HxTextInput, HxCard, HxBadge, HxDialog,} from '@helixui/react';
interface Patient { id: string; name: string; status: 'active' | 'inactive';}
const MOCK_PATIENTS: Patient[] = [ { id: 'P-001', name: 'Jane Smith', status: 'active' }, { id: 'P-002', name: 'Robert Chen', status: 'inactive' }, { id: 'P-003', name: 'Maria Garcia', status: 'active' },];
export function PatientPortal() { const [query, setQuery] = useState(''); const [selected, setSelected] = useState<Patient | null>(null); const [dialogOpen, setDialogOpen] = useState(false);
const filtered = MOCK_PATIENTS.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()) || p.id.includes(query) );
const selectPatient = (patient: Patient) => { setSelected(patient); setDialogOpen(true); };
return ( <div className="portal"> <h1>Patient Portal</h1>
<HxTextInput label="Search patients" placeholder="Name or patient ID" onHxInput={(e) => setQuery((e as CustomEvent<{ value: string }>).detail.value) } />
<div className="patient-list"> {filtered.map((patient) => ( <HxCard key={patient.id}> <h3 slot="heading">{patient.name}</h3> <p>ID: {patient.id}</p> <HxBadge variant={patient.status === 'active' ? 'success' : 'neutral'}> {patient.status} </HxBadge> <HxButton slot="footer" variant="ghost" hx-size="sm" onHxClick={() => selectPatient(patient)} > View record </HxButton> </HxCard> ))} </div>
{selected && ( <HxDialog open={dialogOpen} heading={`Record: ${selected.name}`} onHxClose={() => setDialogOpen(false)} > <dl> <dt>Patient ID</dt> <dd>{selected.id}</dd> <dt>Status</dt> <dd>{selected.status}</dd> </dl> <HxButton slot="footer" variant="primary" onHxClick={() => setDialogOpen(false)} > Close </HxButton> </HxDialog> )} </div> );}Next Steps
Section titled “Next Steps”- Wrapper Strategy overview — architecture and when to use wrappers vs. raw web components
- React Integration — raw web component patterns for React 18+
- Next.js 15 guide — full SSR, Server Actions, and hydration patterns
- Design Tokens — theming HELiX in your React app