Skip to content

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 codeWhat 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 childrenProjects 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 ​

ApplicationImportRegistration owner
Normal React app, SPA, or SSR application@signicat/catnip-components-reactThis import registers Catnip in the browser
Child micro-frontend whose shell already owns Catnip@signicat/catnip-components-react/wrappersThe shell
Raw custom elements without wrappers@signicat/catnip-componentsThe 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.

bash
pnpm add @signicat/catnip-components-react @signicat/catnip-css

The 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:

tsx
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:

tsx
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:

tsx
<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 eventWrapper prop
update:modelValueonUpdateModelValue
open-changeonOpenChange
suffix-clickonSuffixClick
update:openonUpdateOpen

The handler receives the native CustomEvent, not a React SyntheticEvent. Use the exported helper for payload events:

tsx
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:

tsx
<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:

tsx
<CatnipButton>Save</CatnipButton>

Named slots use the standard HTML slot attribute on a light-DOM child:

tsx
<CatnipTooltip>
  <button type="button" slot="anchor">
    Help
  </button>
  A short explanation
</CatnipTooltip>
tsx
<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:

tsx
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:

tsx
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:

tsx
import "@signicat/catnip-css";
import "@signicat/catnip-components-react";

React child micro-frontend:

tsx
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:

tsx
<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 ​

SymptomCheck
Component has no Catnip stylingImport @signicat/catnip-css; inspect whether the host has upgraded and has a Shadow Root
CSS selector does not matchUse className on the wrapper and inspect for a real class="…" attribute
State never updatesHandle the documented generated event and parse its CustomEvent.detail
Object or array looks stringifiedUse the PascalCase wrapper, not a raw lowercase tag with a JSX attribute
Child micro-frontend loads another Catnip versionImport /wrappers; let the shell register components and CSS
Imperative code cannot see shadowRoot yetAwait 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:

bash
cd packages/components-react
pnpm test
pnpm run type-check
pnpm run build

The development-only React playground is available with pnpm run playground:dev.

Catnip Design System by Signicat