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).
Reading prop tables
The Type column describes the JavaScript/TypeScript property value. For finite string props it shows the exact literal union supported by that component, while Options repeats those values in a form that is easier to scan. A shared Catnip vocabulary does not mean that every component accepts every value in it: for example, Checkbox size is "m" | "l", while Button size is "xs" | "s" | "m" | "l".
Two reusable value shapes appear in many component tables:
| Type | Accepted property value | HTML attribute form |
|---|---|---|
SupportingTextProp | A string, an array of strings, one { text, intent?, icon? } message, or an array of those messages | A plain string or a serialized JSON array/object |
TestSelectorsProp | A recursive TestSelectorTree object, or a serialized JSON string | A serialized JSON object string |
Descriptions are shared only when the semantics are shared. Props that happen to have the same name or string values can still describe different component behaviour; always use the type and options from that component's table.
Naming
Keep public props aligned with these jobs:
| Word | Job | Examples |
|---|---|---|
| Bare adjectives | Booleans | open, subtle, disabled, backdropBlur — no is* / has* |
appearance | Visual chrome | Button, Badge, Divider stroke (solid / dashed / …) |
orientation | Horizontal / vertical axis | Stepper, File Input, Divider; Date panelOrientation next to panelPlacement / panelAlign |
layout | Arrangement of the control (not an axis) | Pagination default / compact |
mode | Behavioral / data split | Date single / range |
displayMode | How a value is shown | Dropdown value / chips / count |
intent | Semantic color | Fields, Button, Alert, Toast |
type | Native HTML type | Button, Input |
leftIcon / rightIcon | Directional icons | Button, Input, Chip |
show* | Optional sub-UI | showCloseButton, showClearButton, showValueTooltip |
hideLabel | Field label row | Inputs |
closable | Overlay dismiss policy (Escape / backdrop) | Dialog, Drawer — distinct from showCloseButton |
Qualify geometry when the host already has a family (panelOrientation, not bare orientation on Date Input).
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
Tell the Vue compiler that catnip-* tags are custom elements (isCustomElement). Copy-paste for Vite and Nuxt: Installation — Vue compiler.
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
Prefer the PascalCase wrappers from @signicat/catnip-components-react. Pass the catalogue's camelCase prop names directly; wrappers assign booleans, numbers, objects, and arrays as host properties. Use React className, style, ordinary aria-* / data-* props, and generated event handlers. See the complete React guide.
When wrappers are not available, lowercase catnip-* tags can still be used. In that fallback, pass simple attributes as strings and use a ref plus useEffect for non-string host properties.
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.
Slot naming
Slot names are camelCase (HTML slot="leftIconSlot", never kebab-case). Directional content uses left / right, not leading / trailing.
| Pattern | Rule | Examples |
|---|---|---|
| Icon that overrides a same-named prop | {prop}Slot | leftIcon → leftIconSlot |
| Directional row content (not icon-only) | left / right | List item, dropdown option |
| Section chrome | header / footer (no Slot suffix) | List, Dialog, Drawer |
| Namespaced panel chrome on a parent | camelCase prefix | Dropdown listHeader / listFooter |
| Composed child slots | {child}.{slot} | catnipInput.leftIconSlot |
Field-label anatomy stays action / suffix. Split Button’s extra half uses suffixInactiveIcon / suffixActiveIcon.
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).
Vue + custom elements quirk: @update:modelValue works on catnip-* hosts (Vue preserves the event name when it contains uppercase letters). All-lowercase names like @update:open are not attached.
Controlled open state is split on purpose:
- Modals (Dialog, Drawer):
open+update:open. Listen withaddEventListener("update:open", …). Vue@update:opendoes not attach on custom elements (all-lowercase). Unwrapevent.detail[0] ?? event.detail. - Popovers and fields (Dropdown, Input Date, Input Phone, Tooltip, Split Button):
open+open-change. Use.open+@open-change.
Do not invent an open-sync event. Other events are kebab-case (step-click, sort-click, right-icon-click, close-drawer).
update:modelValue is the canonical two-way value event (Pagination also emits page-change with the same payload; prefer update:modelValue).
React
The React package maps event names to on + PascalCase (update:modelValue → onUpdateModelValue, open-change → onOpenChange). The handler receives the native CustomEvent; use its exported parseCatnipEventDetail helper for documented payload events.
With raw lowercase tags, custom event names (especially those containing :) are awkward in JSX. Use a ref plus addEventListener and read event.detail as documented.
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.