Skip to content

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.

ApproachPackageBest for
React wrappers@loomidev/reactMost React applications. This is the recommended option.
Native custom elements@loomidev/react-typesApplications 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.

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 the wrapper package and Lit:

Terminal window
npm install @loomidev/react lit

React must already be installed in your application. @loomidev/react supports React 18 and React 19.

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 elementReact export
<loomi-button>Button
<loomi-select>Select
<loomi-date-range-picker>DateRangePicker
<loomi-data-grid>DataGrid

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.

ImportRegisters
@loomidev/reactEvery LoomiUI element.
@loomidev/react/button<loomi-button> only.
@loomidev/react/data-grid<loomi-data-grid> only.

Single-word attributes keep the same name. Multi-word attributes use camelCase in the React wrappers:

HTML attributeReact prop
colorcolor
disableddisabled
label-keylabelKey
selected-valueselectedValue
show-focus-ringshowFocusRing

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.

Wrapper event props start with on. Native event names use their familiar React form, while LoomiUI event names are converted to PascalCase.

DOM eventReact wrapper prop
clickonClick
changeonChange
loomi-selectonLoomiSelect
loomi-empty-actiononLoomiEmptyAction
loomi-theme-changeonLoomiThemeChange

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);
}}
/>
);
}

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.

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.

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.

Terminal window
npm install @loomidev/components lit
npm install --save-dev @loomidev/react-types

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.

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.

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.

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" />;
}

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.

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.

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.

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.

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".