React
Use Catnip in React with @signicat/catnip-components-react. The package contains small, generated React wrappers around Catnip's framework-agnostic catnip-* custom elements. It does not contain a second React implementation of the components.
The wrapper's job is to translate React conventions into the custom-element contract. The examples in the second column are simplified equivalents—the wrapper performs this wiring for you:
| React code | What the wrapper does |
|---|---|
modelValue={value} / options={rows} | Assigns the real value to the host property, including objects and arrays. For example: element.modelValue = value and element.options = rows |
onUpdateModelValue={handler} | Adds a listener for the DOM event update:modelValue. This is equivalent to element.addEventListener("update:modelValue", handler) |
className="menu" / style={styles} | Applies React host styling to the actual custom element, roughly producing <catnip-component class="menu" style="…"> |
ref={ref} | Forwards the ref to the catnip-* host element, so ref.current points to the actual <catnip-component> |
| React children | Projects the children into the component's Shadow DOM slots. For example, a child with slot="anchor" is projected into the component's <slot name="anchor"> slot |
Component behaviour, accessibility, props, events, and slots remain defined by the custom elements. Use each component's catalogue pages as the API reference; this page explains the React wiring.
Choose the correct entry point
| Application | Import | Registration owner |
|---|---|---|
| Normal React app, SPA, or SSR application | @signicat/catnip-components-react | This import registers Catnip in the browser |
| Child micro-frontend whose shell already owns Catnip | @signicat/catnip-components-react/wrappers | The shell |
| Raw custom elements without wrappers | @signicat/catnip-components | The application or shell |
Use the default entry unless the application is specifically a child micro-frontend sharing the shell's Catnip instance.
Install
Catnip npm packages currently require access to Signicat's GitLab registry; see Installation — Registry.
pnpm add @signicat/catnip-components-react @signicat/catnip-cssThe React package supports React 18 and 19 as peer dependencies. It pulls in @signicat/catnip-components automatically. If an app pins that package explicitly, keep it on the same version as @signicat/catnip-components-react.
Import the CSS and wrappers from an application entry or shared layout:
import "@signicat/catnip-css";
import { CatnipButton, CatnipToggle } from "@signicat/catnip-components-react";The first default React-package import starts browser-side registration of all catnip-* elements. Do not add a second import "@signicat/catnip-components" in an ordinary React app.
Basic use
Use the PascalCase named exports and camelCase component props:
import { useState } from "react";
import { CatnipButton, CatnipToggle, parseCatnipEventDetail } from "@signicat/catnip-components-react";
export function NotificationSetting() {
const [enabled, setEnabled] = useState(false);
return (
<section>
<CatnipToggle
modelValue={enabled}
onUpdateModelValue={(event) => {
setEnabled(parseCatnipEventDetail<boolean>(event) ?? false);
}}
/>
<CatnipButton appearance="primary" disabled={!enabled}>
Save
</CatnipButton>
</section>
);
}Unlike raw custom-element JSX, wrappers safely pass booleans, numbers, objects, and arrays as DOM properties. Consumers do not need a ref plus useEffect for normal Catnip props.
Controlled values
Catnip does not mutate React state. Pass the current value and handle the documented update event:
<CatnipInput
modelValue={name}
onUpdateModelValue={(event) => {
setName(parseCatnipEventDetail<string>(event) ?? "");
}}
/>The same pattern applies to controlled open: pass open={open} and handle the component's documented onOpenChange or onUpdateOpen event. Do not attach two handlers that both toggle the same state.
Events
Manifest event names become React props using on + PascalCase:
| DOM event | Wrapper prop |
|---|---|
update:modelValue | onUpdateModelValue |
open-change | onOpenChange |
suffix-click | onSuffixClick |
update:open | onUpdateOpen |
The handler receives the native CustomEvent, not a React SyntheticEvent. Use the exported helper for payload events:
const nextValue = parseCatnipEventDetail<string>(event);It accepts both payload shapes used by custom-element tooling: event.detail and a one-element event.detail[0] array. Events without a documented payload can be handled directly.
Classes, inline styles, and ordinary DOM props
Use normal React className and style on wrappers:
<CatnipList className="action-menu" style={{ minWidth: 220 }} role="menu">
{/* rows */}
</CatnipList>They are applied to the custom-element host, which is the element consumer CSS can position and size. The old class wrapper prop is kept as a deprecated compatibility alias; new React code should use className.
Ordinary host props such as id, title, aria-*, and data-* pass through. Component-owned accessibility props shown in catalogue tables use their documented camelCase wrapper names, such as ariaLabel and ariaControls.
Children and slots
Default slots use ordinary React children:
<CatnipButton>Save</CatnipButton>Named slots use the standard HTML slot attribute on a light-DOM child:
<CatnipTooltip>
<button type="button" slot="anchor">
Help
</button>
A short explanation
</CatnipTooltip><CatnipButton>
<CatnipIcon slot="leftIconSlot" name="save" />
Save
</CatnipButton>Use the exact slot name from the component's Specs page. Do not invent render props or React-only slot APIs.
Refs
Wrappers use forwardRef; the ref points to the host custom element:
import { useRef } from "react";
const splitButtonRef = useRef<HTMLElement>(null);
<CatnipSplitButton ref={splitButtonRef}>Save</CatnipSplitButton>;Use refs for focus or genuinely imperative platform integration. Props and generated event handlers should cover ordinary application state.
Server-side rendering
The default entry is safe to import while rendering on a server. It does not execute the browser-only Catnip component bundle until window exists. Server output contains the custom-element hosts and light-DOM children; the browser then registers and upgrades those hosts.
Wrapper property assignment waits for that upgrade, so objects and arrays are not written too early and do not shadow custom-element accessors. When the application should hide unupgraded elements, add @signicat/catnip-components as a direct dependency and import its optional @signicat/catnip-components/foucPrevention.min.css stylesheet before rendering Catnip UI.
Most applications do not need to wait manually. Tests or imperative bootstrap code that must access a component's Shadow DOM immediately can await registration:
import { catnipComponentsReady } from "@signicat/catnip-components-react";
await catnipComponentsReady;catnipComponentsReady resolves immediately during SSR and after the browser component bundle loads on the client. Framework client boundaries are still appropriate for product code that directly accesses window, shadowRoot, layout measurements, or other browser APIs.
Micro-frontends: one Catnip instance
Catnip custom-element names are global to a document. The shell owns the component and CSS version; a child micro-frontend must not register a second runtime copy.
Shell/root application:
import "@signicat/catnip-css";
import "@signicat/catnip-components-react";React child micro-frontend:
import { CatnipButton, CatnipToggle } from "@signicat/catnip-components-react/wrappers";The /wrappers entry exports the same components, types, and parseCatnipEventDetail, but does not import or register @signicat/catnip-components. The child must render in the same document after the shell has selected Catnip. Keep the wrapper API compatible with the shell-owned component version; importing newer wrapper props does not upgrade the shell's elements.
Do not import CSS again in every child. For global configuration, translations, and version checks, use the shell's window.Catnip API as described in Installation — One instance.
TypeScript
The package exports every wrapper's props type, for example CatnipToggleProps, and augments JSX for lowercase catnip-* tags. The Custom Elements Manifest is the source used to regenerate wrapper props and events. Some complex manifest types are intentionally exposed as unknown; follow the component's Specs page for their documented shape.
Without wrappers
Lowercase tags remain available, but React developers then own property assignment and custom-event listeners:
<catnip-button appearance="primary">Save</catnip-button>Use this path only when wrappers are unavailable or a shell exposes the elements without the wrapper-only package. Objects and arrays normally require a ref/property assignment, and event names containing : require addEventListener. See Usage — React.
Troubleshooting
| Symptom | Check |
|---|---|
| Component has no Catnip styling | Import @signicat/catnip-css; inspect whether the host has upgraded and has a Shadow Root |
| CSS selector does not match | Use className on the wrapper and inspect for a real class="…" attribute |
| State never updates | Handle the documented generated event and parse its CustomEvent.detail |
| Object or array looks stringified | Use the PascalCase wrapper, not a raw lowercase tag with a JSX attribute |
| Child micro-frontend loads another Catnip version | Import /wrappers; let the shell register components and CSS |
Imperative code cannot see shadowRoot yet | Await catnipComponentsReady or customElements.whenDefined(tagName) |
Local package development
Wrappers are regenerated from @signicat/catnip-components/custom-elements.json by pnpm run gen. After changing a component API, build the components package, regenerate the wrappers, and run:
cd packages/components-react
pnpm test
pnpm run type-check
pnpm run buildThe development-only React playground is available with pnpm run playground:dev.