Component API
Catnip components are custom elements. Their public surface is props (HTML attributes and IDL properties), slots, DOM events, and optional test selectors (data-test via testSelectors). For a quick tour across frameworks, see Usage.
Props
Tables on each component page list names in camelCase as used in TypeScript. In plain HTML, use kebab-case attributes (for example modelValue → model-value).
Attributes vs properties
- HTML — set attributes (
model-value,disabled,size="m"). Boolean props are often presence-based (disabledwith no value). - JavaScript — read or set properties on the element:
el.modelValue,el.disabled. - With automatic type conversion enabled (default), string attributes like
size="48"are coerced where the component expects a number.
Complex values (for example testSelectors) are passed as JSON strings on the attribute, or as objects when your framework binds properties (see Test selectors).
Composed components
When one Catnip component embeds another Catnip component and exposes part of the child API, forwarded props use a nested property named after the child component. The child prop names stay unchanged inside that object.
<catnip-input-chip .catnipInput="{ placeholder: 'Add a tag', leftIcon: 'search' }" />Parent-owned behaviour remains top-level. For example, catnip-input-chip owns modelValue as the committed tag list, while the embedded text field props live under catnipInput. The parent may still reserve or override child props needed for its own invariants, such as an inner modelValue, type, hideLabel, or form name.
Vue 3
In SFC templates, any reactive / non-literal bind on a catnip-* host should use Vue’s leading-dot shorthand in camelCase (for example .modelValue="x", .size="n", .testSelectors="obj"). That sets the DOM property on the host. Do not use kebab-case :model-value or plain :modelValue / :size for those values — they go through the attribute path and break or stringify incorrectly for custom elements.
For values that must be DOM properties (booleans, numbers, objects, arrays), the same leading-dot form applies. See Vue — Passing DOM properties. Avoid spelling out :modelValue.prop in templates unless you have a rare reason to.
modelValue: .modelValue + @update:modelValue, unwrap detail per the component (often detail[0]). Do not use v-model on the catnip-* host — it is not the same as v-model on Vue components. Where documented, use v-catnip-model instead; see Configuration and each component’s Props / Events.
React
Use lowercase catnip-* tags and pass attributes as strings (or booleans where React maps them correctly). For non-string props that must be properties, use a ref and useEffect to assign properties, or pass serialised JSON for props that support it.
Angular
Use [attr.*] for string attributes or CUSTOM_ELEMENTS_SCHEMA and property bindings as appropriate for your version.
Slots
Catnip components use the standard Shadow DOM slot model: the component defines named and default slots in its shadow tree; you fill them by placing light DOM children on the custom element and marking which slot each child belongs to.
Named and default slots
- Default slot — children with no
slotattribute (orslot="") are projected into the component’s unnamed<slot>. - Named slots — use the
slotattribute on a child element to match a<slot name="…">in the component, for exampleslot="anchor"oncatnip-tooltip.
<catnip-tooltip>
<button type="button" slot="anchor">Hover me</button>
Short hint text
</catnip-tooltip>Multiple nodes can target the same named slot when the component is designed for it; see each component’s Slots table.
Forwarded child slots
Composed components use the same namespace for forwarded child slots. The name is still a normal Shadow DOM slot name; the dot is the Catnip convention that means “this slot belongs to the embedded component”.
<catnip-input-chip>
<span slot="catnipInput.leftIconSlot">#</span>
</catnip-input-chip>For the underlying platform behaviour (slottable nodes, fallbacks, composition), see MDN: Adding flexibility with slots.
React and JSX
Use the slot prop (maps to the slot attribute):
<catnip-tooltip>
<button type="button" slot="anchor">Hover me</button>
Short hint text
</catnip-tooltip>Events
Catnip custom elements emit DOM events (CustomEvent where a payload is needed). You listen on the host element like any other DOM node.
Vanilla JavaScript
el.addEventListener("remove", (event) => {
// event.detail when provided
});Use the event name documented on each component (for example remove, update:modelValue). Payloads, if any, are on event.detail.
Vue 3
Use @eventName in templates (for example @remove, @update:modelValue). Vue passes the native DOM event to your handler; read event.detail when the docs specify a payload.
Some Catnip events normalize detail for framework interop (for example a one-element array). Prefer the v-catnip-model directive for update:modelValue where documented, or unwrap detail in the handler as described on the component page (see Toggle).
React
Custom event names (especially with :) are awkward in JSX. Prefer a ref and addEventListener on the host, or a small wrapper component. Read event.detail as in the component docs.
Composed events
If an event is dispatched with composed: true, it crosses the shadow boundary; otherwise listening on the host is still the usual approach for component-level APIs.
Test selectors
Catnip components expose stable data-test hooks for end-to-end and integration tests. Defaults are defined per component; you can override them with the testSelectors prop without changing internal markup.
How it works
The prop maps internal keys (for example root, anchor, content) to the string value written on data-test. Keys you omit keep the component’s built-in defaults.
Some keys accept a nested plain object instead of a string. Those values are forwarded to an embedded Catnip component as its own testSelectors and merged with that child’s defaults (the same namespace idea as forwarded props such as catnipInput on catnip-input-chip). Where documented, a string in that slot is shorthand for { root: "<string>" } on the child.
Multiple subcomponents of the same type
Different roles — If the parent renders several instances of the same component type but they do different jobs (for example Add, Show more, and Show less on catnip-input-chip), expose one key per role, not one key per component type. Each key can be a string (usually the child’s root selector) or a full nested object so tests can override inner parts of that child (see that component’s Test Selectors table) without affecting the others.
Same role, many instances (lists, rails, chips) — Prefer one shared selector on every repeated child and distinguish items in tests with DOM order (:nth-of-type), visible text, or ARIA (aria-selected, aria-current, and similar). Alternatively, put a stable data-test on a wrapper and query descendants inside it. Use index suffixes in selector strings only when you truly cannot target rows reliably (for example some virtualised lists); suffixes couple tests to order and need explicit documentation on the component.
Attributes vs properties — Nested testSelectors trees must stay JSON-serialisable if you pass them through an HTML attribute (test-selectors='…'). Prefer .testSelectors (property binding) in Vue when the object is large or built in code.
HTML and plain JS
Pass a JSON string on the attribute test-selectors:
<catnip-tooltip test-selectors='{"root":"my-tooltip","content":"my-tooltip-body"}'>
Help text
</catnip-tooltip>Vue and other frameworks
In Vue SFCs, bind the property with .testSelectors (leading dot, camelCase), not :test-selectors. See Vue 3.
You can bind an object when the bundler passes properties to the custom element (same semantics as the JSON string):
<catnip-tooltip .testSelectors="{ root: 'my-tooltip', content: 'my-tooltip-body' }">
Help text
</catnip-tooltip>- Object — override only the keys you need; the rest stay at defaults.
- String — use when setting attributes in HTML or when your stack only forwards attribute strings.
Each component documents its keys and default values under Test Selectors on its catalogue page. Composed components may document extra keys for forwarded children (for example catnipInput on Chip Input — Specs).
See Configuration — Test attributes for how this fits with global CatnipConfig.