Component Api Design logo

Component Api Design

Organization
staple-lab
component-api-design

Use when designing or reviewing a component's props API - variants, sizes, controlled vs uncontrolled state, compound components, polymorphism, escape hatches, TypeScript prop types, ref forwarding, and naming. Triggers on "component API", "props design", "should this be a prop or a slot", "how do I type this component".

Overview

Publisherstaple-lab
Repositorydesign-system-skills
Skill namecomponent-api-design
Stars
7
Forks
0
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by staple-lab on GitHub. Read the source before you install it.

Installation

Install the Component Api Design AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/staple-lab/design-system-skills/tree/main/plugins/design-system/skills/component-api-design
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/staple-lab/design-system-skills.git /tmp/design-system-skills
mkdir -p .claude/skills
cp -r /tmp/design-system-skills/plugins/design-system/skills/component-api-design .claude/skills/component-api-design
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Component Api Design in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Component Api Design on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Component Api Design is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Component API design

The props API is the public contract. Styles can be fixed in an afternoon; a bad API is in a hundred call sites and stays wrong for years. Spend the extra hour here.

Boolean props are a trap

The most common API mistake, and it compounds:

tsx
// Bad — every new state doubles the invalid combinations
<Button primary secondary danger large small loading disabled />

// Good — enumerated, mutually exclusive by construction
<Button variant="primary" size="lg" loading />

variant and size should be string unions. Booleans are for genuinely independent binary states (loading, disabled, fullWidth). The test: if two props can never be true at once, they are one prop with a union type.

Controlled, uncontrolled, or both

Both — and the convention is non-negotiable because consumers expect DOM semantics:

tsx
interface Props {
  value?: string;              // controlled
  defaultValue?: string;       // uncontrolled initial
  onValueChange?: (v: string) => void;   // fires in BOTH modes
}

Rules:

  • value present → controlled. The component never updates its own state; it renders what it is given.
  • value absent → uncontrolled, defaultValue seeds it.
  • onValueChange fires in both modes. A callback that only fires when uncontrolled is a bug people hit at the worst time.
  • Never switch modes mid-life. Warn in dev if value goes from undefined to defined.
  • Name it onValueChange, not onChange, when it emits a value rather than an event — onChange implies a React ChangeEvent, and the mismatch bites at the first e.target.value.

Most primitive libraries already implement this correctly. Pass the props straight through and do not add a useState inside the wrapper; that is how wrappers break the controlled case.

Compound components

When a component has parts a consumer needs to arrange, reorder or replace, expose the parts:

tsx
<Dialog.Root>
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Content>
    <Dialog.Title>Delete project</Dialog.Title>
    <Dialog.Description>This cannot be undone.</Dialog.Description>
    <Dialog.Footer>
      <Dialog.Close>Cancel</Dialog.Close>
      <Button variant="danger">Delete</Button>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog.Root>

The alternative — <Dialog title="..." description="..." footer={<>...</>} /> — looks tidier and dies on the first requirement that does not fit the props you imagined. A component with three or more ReactNode props is a compound component wearing a disguise.

When NOT to go compound: a Button with an icon does not need Button.Icon. Use compound parts when the arrangement varies, props when only the content varies.

Both can coexist: ship <Dialog> as a convenience wrapper over the parts for the 80% case, and export the parts for the rest. Make sure the convenience version is implemented using the parts, or the two drift.

Slots

For content in fixed positions, ReactNode props are right:

tsx
interface TextFieldProps {
  label: ReactNode;
  description?: ReactNode;
  errorMessage?: ReactNode;
  startAdornment?: ReactNode;   // "adornment"/"slot", not "icon" — people put buttons and badges there
  endAdornment?: ReactNode;
}

Type them ReactNode, not string. The moment someone needs <label>Name <Required/></label> a string type forces a fork.

Polymorphism

One convention per system (conventions.polymorphism in the brief). Whichever it is, the typing needs to preserve the target element's props:

tsx
// `render` (Base UI style)
<Button render={<a href="/pricing" />}>See pricing</Button>

// `asChild` (Radix style)
<Button asChild><a href="/pricing">See pricing</a></Button>

Do not invent a third. And prefer polymorphism over a href prop — the moment you add href you are reimplementing an anchor, and you will get target, rel, download and router integration wrong in that order.

TypeScript

tsx
import type { ComponentPropsWithRef, ReactNode } from 'react';
import type { VariantProps } from 'class-variance-authority';

export interface ButtonProps
  extends ComponentPropsWithRef<'button'>,          // inherit every native prop, including ref (React 19)
    VariantProps<typeof buttonVariants> {           // variant/size derived from the styles — one source
  loading?: boolean;
  startIcon?: ReactNode;
}
  • Extend the native element props. Consumers will need type="submit", form, aria-*, data-testid. Re-declaring a subset guarantees you missed one.
  • Derive variant types from the style definition so adding a variant to CVA cannot desync from the type.
  • Export every prop type. Consumers wrap your components; without the type they write any.
  • Use interface for public props (better error messages, declaration merging) and type for unions.

Encode invariants in the type rather than documenting them. An icon-only button that requires a label:

tsx
type ButtonProps = BaseProps &
  ({ children: ReactNode; 'aria-label'?: string } | { children?: never; 'aria-label': string });

Now the unlabelled icon button does not compile. That is worth more than a lint rule and infinitely more than a docs note.

Defaults

Every optional prop needs a sensible default so the zero-config case is the common case. <Button>Save</Button> should render the primary button at the default size. If a component cannot render without four props, the API is wrong.

Defaults belong in one place — the CVA/recipe defaultVariants or the destructure, not both.

Naming

DoDon'tWhy
variant, size, tonetype, style, kindtype and style collide with native props
isOpen / openvisible, shown, activeMatch the DOM/ARIA vocabulary
onValueChangeonUpdate, handleChangeon + noun + Change reads at the call site
disabledisDisabled, enabledMatch the native attribute; never invert a native boolean
startIcon / endIconleftIcon / rightIconLeft and right swap in RTL; start and end do not

Consistency beats individual elegance. If the system already uses isOpen, the next component uses isOpen.

Escape hatches

Every component needs exactly one deliberate way out, or people fork it:

  1. className passthrough, merged not replaced (cn(styles.base, className))
  2. ...rest spread onto the outer element
  3. ref forwarded
  4. Component-level CSS custom properties for the values most likely to need overriding

The failure mode is silent: a developer needs a 2px adjustment, finds no way in, copies the component into their feature folder, and now there are two buttons. You never hear about it.

The review checklist

Before a component is called done:

  • No boolean props that should be a union
  • Controlled and uncontrolled both work, onXChange fires in both
  • Extends native element props; ref reaches a real DOM node
  • className merged, ...rest spread, both verified by a test
  • Every variant × size × state combination renders (that is a test, not an eyeball)
  • Disabled state is not focusable-but-interactive, and communicates why when it can
  • Loading state preserves layout (no reflow when a spinner replaces a label)
  • Keyboard operable end to end; focus visible at 3:1
  • Works in both themes and at 200% browser zoom
  • Prop types exported
  • Registry entry filled in — props, a11y notes, do/don't, tokens consumed

Frequently asked questions

What does the Component Api Design AI skill do?

Use when designing or reviewing a component's props API - variants, sizes, controlled vs uncontrolled state, compound components, polymorphism, escape hatches, TypeScript prop types, ref forwarding, and naming. Triggers on "component API", "props design", "should this be a prop or a slot", "how do I type this component".

Why use Component Api Design on TypingMind?

Because you install it once and use it with any model. Component Api Design is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Component Api Design in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/staple-lab/design-system-skills/tree/main/plugins/design-system/skills/component-api-design. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Component Api Design?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Component Api Design?

As many as you like. As long as a model supports skills, you can use Component Api Design with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Component Api Design AI skill free?

Yes. It is published on GitHub by staple-lab under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇