Skip to content

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:

TypeAccepted property valueHTML attribute form
SupportingTextPropA string, an array of strings, one { text, intent?, icon? } message, or an array of those messagesA plain string or a serialized JSON array/object
TestSelectorsPropA recursive TestSelectorTree object, or a serialized JSON stringA 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:

WordJobExamples
Bare adjectivesBooleansopen, subtle, disabled, backdropBlur — no is* / has*
appearanceVisual chromeButton, Badge, Divider stroke (solid / dashed / …)
orientationHorizontal / vertical axisStepper, File Input, Divider; Date panelOrientation next to panelPlacement / panelAlign
layoutArrangement of the control (not an axis)Pagination default / compact
modeBehavioral / data splitDate single / range
displayModeHow a value is shownDropdown value / chips / count
intentSemantic colorFields, Button, Alert, Toast
typeNative HTML typeButton, Input
leftIcon / rightIconDirectional iconsButton, Input, Chip
show*Optional sub-UIshowCloseButton, showClearButton, showValueTooltip
hideLabelField label rowInputs
closableOverlay 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 (disabled with 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.

vue
<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 slot attribute (or slot="") are projected into the component’s unnamed <slot>.
  • Named slots — use the slot attribute on a child element to match a <slot name="…"> in the component, for example slot="anchor" on catnip-tooltip.
html
<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.

PatternRuleExamples
Icon that overrides a same-named prop{prop}SlotleftIcon → leftIconSlot
Directional row content (not icon-only)left / rightList item, dropdown option
Section chromeheader / footer (no Slot suffix)List, Dialog, Drawer
Namespaced panel chrome on a parentcamelCase prefixDropdown 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”.

html
<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):

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

js
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 with addEventListener("update:open", …). Vue @update:open does not attach on custom elements (all-lowercase). Unwrap event.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:

html
<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):

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

Catnip Design System by Signicat