React
LoomiUI works in React through standard custom elements. You can use the React wrapper
package for a familiar component API, or render the underlying loomi-* elements
directly.
For most React applications, start with @loomidev/react. It gives you named React
components, typed props, callback props for LoomiUI events, and safe handling for arrays
and objects in both React 18 and React 19.
Choose an approach
Section titled “Choose an approach”| Approach | Package | Best for |
|---|---|---|
| React wrappers | @loomidev/react | Most React applications. This is the recommended option. |
| Native custom elements | @loomidev/react-types | Applications that want to write <loomi-*> tags directly. |
Both approaches use the same LoomiUI components. The difference is how React passes properties and listens for events.
React wrappers
Section titled “React wrappers”The wrapper package maps every LoomiUI custom element to a named React component. It also registers the underlying custom elements, so you do not need separate component imports.
Install
Section titled “Install”Install the wrapper package and Lit:
npm install @loomidev/react litReact must already be installed in your application. @loomidev/react supports React
18 and React 19.
Render a component
Section titled “Render a component”Import named components from @loomidev/react:
import { Button } from "@loomidev/react";
export function SaveButton() { return ( <Button color="primary" onClick={() => console.log("Saved")}> Save changes </Button> );}Each export uses the custom element name without the loomi- prefix and converts it to
PascalCase:
| Custom element | React export |
|---|---|
<loomi-button> | Button |
<loomi-select> | Select |
<loomi-date-range-picker> | DateRangePicker |
<loomi-data-grid> | DataGrid |
Import only the components you use
Section titled “Import only the components you use”Importing from @loomidev/react registers every LoomiUI element, which is convenient
but pulls the whole library into your bundle. Each component also has its own entry
point, named after the custom element without the loomi- prefix:
import { Button } from "@loomidev/react/button";import { DataGrid } from "@loomidev/react/data-grid";The components are identical either way. A per-component import registers only that element, so an application that uses a handful of components ships a handful of components.
| Import | Registers |
|---|---|
@loomidev/react | Every LoomiUI element. |
@loomidev/react/button | <loomi-button> only. |
@loomidev/react/data-grid | <loomi-data-grid> only. |
Pass props
Section titled “Pass props”Single-word attributes keep the same name. Multi-word attributes use camelCase in the React wrappers:
| HTML attribute | React prop |
|---|---|
color | color |
disabled | disabled |
label-key | labelKey |
selected-value | selectedValue |
show-focus-ring | showFocusRing |
Arrays and objects can be passed directly. The wrapper assigns them to the custom element as JavaScript properties instead of serializing them as HTML attributes.
import { useState } from "react";import { Select } from "@loomidev/react";
const countries = [ { name: "Canada", code: "ca" }, { name: "Ghana", code: "gh" }, { name: "India", code: "in" },];
export function CountryField() { const [country, setCountry] = useState("gh");
return ( <Select name="country" label="Country" data={countries} labelKey="name" valueKey="code" selectedValue={country} onLoomiSelect={(event) => setCountry(event.detail.value)} /> );}This pattern behaves consistently in React 18 and React 19.
Handle events
Section titled “Handle events”Wrapper event props start with on. Native event names use their familiar React form,
while LoomiUI event names are converted to PascalCase.
| DOM event | React wrapper prop |
|---|---|
click | onClick |
change | onChange |
loomi-select | onLoomiSelect |
loomi-empty-action | onLoomiEmptyAction |
loomi-theme-change | onLoomiThemeChange |
The component reference lists the events available for each component. For example, see the Select events.
When an event is a CustomEvent, TypeScript exposes its typed detail value:
import { Select } from "@loomidev/react";
export function StatusField() { return ( <Select label="Status" data={[ { label: "Active", value: "active" }, { label: "Paused", value: "paused" }, ]} onLoomiSelect={(event) => { console.log(event.detail.value); console.log(event.detail.label); }} /> );}Access the custom element with a ref
Section titled “Access the custom element with a ref”Every wrapper forwards its ref to the underlying custom element. Use a ref when you need to call a public component method, focus the element, or read a property that is not part of your React state.
import { useRef } from "react";import { Button } from "@loomidev/react";import type { LoomiButton } from "@loomidev/button";
export function AsyncSaveButton() { const buttonRef = useRef<LoomiButton>(null);
async function save() { buttonRef.current?.startSpinner();
try { await updateAccount(); } finally { buttonRef.current?.stopSpinner(); } }
return ( <Button ref={buttonRef} color="primary" hasSpinner onClick={save}> Save changes </Button> );}The component reference documents the public methods available on each element.
Use components in a form
Section titled “Use components in a form”Form-capable LoomiUI components participate in the browser’s native form system. Give
the component a name, then read it with FormData like a native input.
import type { FormEvent } from "react";import { Button, Input, Select } from "@loomidev/react";
export function ProfileForm() { function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const data = new FormData(event.currentTarget);
console.log(data.get("displayName")); console.log(data.get("role")); }
return ( <form onSubmit={submit}> <Input name="displayName" label="Display name" required /> <Select name="role" label="Role" data={[ { label: "Admin", value: "admin" }, { label: "Member", value: "member" }, ]} required /> <Button type="submit" color="primary"> Create user </Button> </form> );}Check the individual component page to confirm whether a component is form-associated and which value it submits.
Native custom elements
Section titled “Native custom elements”Use this option when you prefer <loomi-*> tags and do not need React wrapper
components. The @loomidev/react-types package adds JSX autocomplete and type checking
without adding runtime behavior.
Install
Section titled “Install”npm install @loomidev/components litnpm install --save-dev @loomidev/react-typesConfigure TypeScript
Section titled “Configure TypeScript”Add @loomidev/react-types to compilerOptions.types in your application’s
tsconfig.json:
{ "compilerOptions": { "types": ["@loomidev/react-types"] }}If your types array already contains other packages, keep them and append
@loomidev/react-types.
Register and render an element
Section titled “Register and render an element”Import the component package before rendering its tag:
import "@loomidev/components/button";
export function SaveButton() { return ( <loomi-button color="primary" disabled={false}> Save changes </loomi-button> );}Import @loomidev/components once in your client entry file if you want to register
every component. Import a component subpath such as @loomidev/components/button when
you only want to register specific elements.
Pass arrays and objects
Section titled “Pass arrays and objects”React 19 can pass arrays and objects to custom element properties directly:
import "@loomidev/components/select";
const countries = [ { label: "Canada", value: "ca" }, { label: "Ghana", value: "gh" },];
export function CountryField() { return <loomi-select label="Country" data={countries} />;}React 18 serializes non-primitive values when they are passed to an unknown element. Assign arrays, objects, and functions through a ref instead:
import { useEffect, useRef } from "react";import "@loomidev/components/select";
const countries = [ { label: "Canada", value: "ca" }, { label: "Ghana", value: "gh" },];
export function CountryField() { const selectRef = useRef<HTMLElementTagNameMap["loomi-select"]>(null);
useEffect(() => { if (selectRef.current) { selectRef.current.data = countries; } }, []);
return <loomi-select ref={selectRef} label="Country" />;}Use the React wrappers if your application needs to support both React 18 and React 19. The wrappers handle property assignment for you.
Listen for custom events
Section titled “Listen for custom events”With native custom elements, addEventListener provides consistent behavior across
React versions and gives you access to the typed LoomiUI event map.
import { useEffect, useRef } from "react";import "@loomidev/components/select";
export function CountryField({ onSelect }: { onSelect: (value: string) => void }) { const selectRef = useRef<HTMLElementTagNameMap["loomi-select"]>(null);
useEffect(() => { const select = selectRef.current; if (!select) return;
const handleSelect = (event: CustomEvent<{ value: string }>) => { onSelect(event.detail.value); };
select.addEventListener("loomi-select", handleSelect); return () => select.removeEventListener("loomi-select", handleSelect); }, [onSelect]);
return <loomi-select ref={selectRef} label="Country" />;}Server-rendered React applications
Section titled “Server-rendered React applications”Importing a LoomiUI component on the server is safe. Lit registers its elements through
a server-side DOM shim, so a server build or prerender does not fail on a missing
browser global. The server renders the loomi-* tag, and the element upgrades itself
once it reaches the browser.
The components are interactive, so they still belong in client-side code. For the Next.js App Router, put LoomiUI usage in a Client Component:
"use client";
import { Button } from "@loomidev/react";
export function ClientSaveButton() { return <Button color="primary">Save changes</Button>;}Keep data fetching and other server work in a Server Component, then pass serializable props into the Client Component that renders LoomiUI.
Styling and theming
Section titled “Styling and theming”Wrapper components and native custom elements use the same CSS custom properties. Put theme overrides in your global stylesheet:
:root { --loomi-primary-600: #2563eb; --loomi-primary-700: #1d4ed8;}See Theming for color tokens, dark mode, and Tailwind integration.
Troubleshooting
Section titled “Troubleshooting”TypeScript does not recognize a loomi-* tag
Section titled “TypeScript does not recognize a loomi-* tag”Install @loomidev/react-types and add it to compilerOptions.types. Restart the
TypeScript server after changing tsconfig.json if your editor still shows the old
diagnostic.
An array renders as [object Object]
Section titled “An array renders as [object Object]”This usually means React 18 serialized an array or object as an attribute. Use the React wrapper package, or assign the value through a ref.
A LoomiUI event callback does not run
Section titled “A LoomiUI event callback does not run”With wrappers, use the event prop shown in the component reference, such as
onLoomiSelect. With native custom elements, attach the exact DOM event name with
addEventListener.
A server build reports that a browser global is missing
Section titled “A server build reports that a browser global is missing”LoomiUI imports themselves are server-safe, so check your own code first: reading
window, document, or localStorage during render is the usual cause. Move that work
into an effect or into client-side code. In Next.js, mark the component with
"use client".
Next steps
Section titled “Next steps”- Browse components to find props, events, methods, and examples.
- Configure theming to match LoomiUI to your product.
- Review installation options if you want to use native custom elements.
- Open the playground to test component properties interactively.