Skip to content
SP StackPractices
intermediate By Mathias Paulenko

Component API Documentation Template

A template for documenting UI component APIs: props, events, slots, methods, accessibility, and usage examples with TypeScript types.

Topics: testing

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

Component API documentation tells developers how to use a component: what props it accepts, what events it emits, what slots it provides, and what methods it exposes. Without clear API docs, developers read source code to understand components, wasting time and leading to incorrect usage.

When to Use

  • For alternatives, see Browser Support Matrix Template.

  • Documenting design system components

  • Publishing a component library

  • Onboarding new developers to a component system

  • Creating component usage guidelines

  • Maintaining API consistency across components

Solution

# Component API — `<Button>`

## Component Overview

| Field | Value |
|-------|-------|
| Component Name | Button |
| Package | @example/ui-components |
| Version | 2.3.0 |
| Status | Stable |
| Last Updated | 2026-07-05 |
| Maintainer | UI Platform Team |
| Source | src/components/Button/Button.tsx |
| Bundle Size (gzipped) | 2.1 KB |
| Dependencies | none |

## Description

A versatile button component with support for multiple variants, sizes, icons, loading states, and full keyboard navigation. Renders as a native `<button>` element by default, with polymorphic rendering via the `as` prop.

## Installation

```bash
npm install @example/ui-components

Basic Usage

import { Button } from '@example/ui-components';

function Example() {
  return <Button onClick={() => alert('Clicked!')}>Click me</Button>;
}

Props

PropTypeDefaultRequiredDescription
variant'primary' | 'secondary' | 'ghost' | 'danger' | 'link''primary'NoVisual style variant
size'sm' | 'md' | 'lg''md'NoButton size
disabledbooleanfalseNoDisables interaction and applies disabled styles
loadingbooleanfalseNoShows loading spinner and disables interaction
fullWidthbooleanfalseNoMakes button take full width of container
iconReactNodeundefinedNoIcon element rendered before children
iconPosition'left' | 'right''left'NoPosition of icon relative to text
type'button' | 'submit' | 'reset''button'NoNative button type attribute
as'button' | 'a' | React.ElementType'button'NoPolymorphic rendering element
hrefstringundefinedNoURL when as="a"; required for link rendering
targetstringundefinedNoLink target attribute when as="a"
relstringundefinedNoLink rel attribute when as="a"
ariaLabelstringundefinedNoAccessible label for icon-only buttons
testIdstringundefinedNoData attribute for testing: data-testid
classNamestringundefinedNoAdditional CSS classes
styleCSSPropertiesundefinedNoInline styles
onClick(e: MouseEvent) => voidundefinedNoClick handler
onFocus(e: FocusEvent) => voidundefinedNoFocus handler
onBlur(e: FocusEvent) => voidundefinedNoBlur handler

TypeScript Types

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  loading?: boolean;
  fullWidth?: boolean;
  icon?: ReactNode;
  iconPosition?: 'left' | 'right';
  type?: 'button' | 'submit' | 'reset';
  as?: 'button' | 'a' | ElementType;
  href?: string;
  target?: string;
  rel?: string;
  ariaLabel?: string;
  testId?: string;
  className?: string;
  style?: CSSProperties;
  onClick?: (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
  onFocus?: (e: FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
  onBlur?: (e: FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
  children?: ReactNode;
}

Prop Details

variant

Controls the visual style of the button. Each variant has distinct colors for default, hover, active, and disabled states.

VariantUse CaseDefault BGText ColorBorder
primaryMain action on a pagebrand-600whitenone
secondaryAlternative actionwhiteslate-700slate-300
ghostTertiary actiontransparentslate-700none
dangerDestructive actionred-600whitenone
linkNavigation-style actiontransparentbrand-600none
<Button variant="primary">Save changes</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost">More options</Button>
<Button variant="danger">Delete account</Button>
<Button variant="link" as="a" href="/docs">Read documentation</Button>

size

Controls the height, padding, and font size of the button.

SizeHeightPadding (x, y)Font SizeIcon Size
sm32px12px, 6px14px16px
md40px16px, 8px16px20px
lg48px24px, 12px18px24px
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>

loading

When true, the button shows a spinner, disables interaction, and maintains its width to prevent layout shift.

<Button loading={isSubmitting}>Submit</Button>

as (Polymorphic)

Renders the button as a different element while maintaining styling. Common use: rendering as an anchor for navigation.

<Button as="a" href="/dashboard" variant="primary">Go to Dashboard</Button>
<Button as="a" href="/report.pdf" target="_blank" rel="noopener noreferrer">
  Download Report
</Button>

Events

EventPayloadDescription
onClickMouseEventFired when button is clicked (not when disabled or loading)
onFocusFocusEventFired when button receives focus
onBlurFocusEventFired when button loses focus

Event Examples

function FormExample() {
  const [loading, setLoading] = useState(false);

  const handleClick = async (e: MouseEvent) => {
    e.preventDefault();
    setLoading(true);
    await submitForm();
    setLoading(false);
  };

  return (
    <Button onClick={handleClick} loading={loading}>
      Submit
    </Button>
  );
}

Slots

SlotDefaultDescription
childrenButton label text or content
iconundefinedIcon rendered before or after children based on iconPosition

Slot Examples

// Text only
<Button>Save</Button>

// Text with left icon
<Button icon={<SaveIcon />}>Save</Button>

// Text with right icon
<Button icon={<ArrowIcon />} iconPosition="right">Next</Button>

// Icon only (requires ariaLabel)
<Button icon={<CloseIcon />} ariaLabel="Close dialog" />

// Custom content
<Button>
  <span className="flex items-center gap-2">
    <Badge>New</Badge>
    Try Pro
  </span>
</Button>

Methods

The Button component does not expose imperative methods. All interaction is handled through props and events.

Accessibility

AttributeValueNotes
Rolebutton (native)Uses native <button> element
KeyboardEnter/SpaceTriggers onClick
FocusVisible outlinefocus-visible styles applied
Disabledaria-disabledSet when disabled or loading is true
Labelaria-labelRequired for icon-only buttons via ariaLabel prop

Accessibility Examples

// Icon-only button — ariaLabel is required
<Button icon={<SearchIcon />} ariaLabel="Search" />

// Loading state — aria-label updated
<Button loading={isLoading} ariaLabel={isLoading ? 'Submitting...' : 'Submit'}>
  Submit
</Button>

// Disabled state
<Button disabled ariaLabel="Save (disabled)">Save</Button>

Variants Showcase

// Primary actions
<Button variant="primary">Save changes</Button>
<Button variant="primary" icon={<PlusIcon />}>New project</Button>

// Secondary actions
<Button variant="secondary">Cancel</Button>
<Button variant="secondary" icon={<DownloadIcon />} iconPosition="right">Export</Button>

// Ghost actions
<Button variant="ghost">More options</Button>
<Button variant="ghost" size="sm">Filter</Button>

// Danger actions
<Button variant="danger">Delete</Button>
<Button variant="danger" loading={isDeleting}>Deleting...</Button>

// Link variant
<Button variant="link" as="a" href="/docs">Documentation</Button>
<Button variant="link" as="a" href="/docs" target="_blank" rel="noopener noreferrer">
  External link
</Button>

// Full width
<Button fullWidth>Submit form</Button>

// All sizes
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>

Do’s and Don’ts

DoDon’t
Use primary for the main action on a pageUse multiple primary buttons in the same view
Provide ariaLabel for icon-only buttonsUse icon-only buttons without accessible labels
Use loading instead of disabling + text changeChange button text to “Loading…” manually
Use as="a" for navigationUse onClick with window.location for navigation
Use danger for destructive actionsUse danger for non-destructive actions
Use fullWidth in mobile layoutsUse fullWidth in desktop layouts unnecessarily

Testing

import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '@example/ui-components';

describe('Button', () => {
  it('renders children', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
  });

  it('fires onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click</Button>);
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('does not fire onClick when disabled', () => {
    const handleClick = jest.fn();
    render(<Button disabled onClick={handleClick}>Click</Button>);
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).not.toHaveBeenCalled();
  });

  it('does not fire onClick when loading', () => {
    const handleClick = jest.fn();
    render(<Button loading onClick={handleClick}>Click</Button>);
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).not.toHaveBeenCalled();
  });

  it('renders as anchor when as="a"', () => {
    render(<Button as="a" href="/home">Home</Button>);
    expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/home');
  });

  it('applies aria-label for icon-only buttons', () => {
    render(<Button icon={<SearchIcon />} ariaLabel="Search" />);
    expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument();
  });
});

Migration Guide

v1.x to v2.x

v1 Propv2 PropBreaking Change
colorvariantRenamed for clarity
blockfullWidthRenamed for consistency
isLoadingloadingSimplified naming
iconLefticon + iconPosition="left"Merged into single prop
iconRighticon + iconPosition="right"Merged into single prop

## Explanation

Component API documentation serves two audiences: developers who use the component and developers who maintain it. Users need to know what props exist, what types they accept, and what the defaults are. Maintainers need to track changes, deprecations, and migration paths.

The props table is the core of the documentation. Each prop should have a TypeScript type, a default value, whether it's required, and a description. For union types (like `variant`), list all valid values. For complex props, add a detailed subsection with examples.

Events document the callback props. Each event should specify the payload type and when it fires. Note edge cases: does `onClick` fire when the button is disabled? (It shouldn't.)

Slots document where content can be projected. In React, this is `children` and render props. In Vue, this is named slots. In Angular, this is `ng-content` with selectors. Document what content is expected and how it's positioned.

The accessibility section is non-negotiable. Every component should document its keyboard interactions, ARIA attributes, and required labels. Icon-only buttons must document the `ariaLabel` requirement.

The testing section shows how to test the component. This helps consumers write integration tests that include the component. It also serves as executable documentation — the tests verify the documented behavior.

The migration guide helps with version upgrades. List every breaking change with the old API and the new API. This reduces upgrade friction.

## Variants

| Context | Approach | Notes |
|---------|----------|-------|
| React component | Props, events as callbacks, children as slots | TypeScript interfaces |
| Vue component | Props, emits, slots | DefineProps, defineEmits |
| Angular component | @Input, @Output, ng-content | Angular decorators |
| Web component | Attributes, custom events, slots | Custom elements spec |
| Svelte component | Props, dispatch events, slots | Svelte stores |

## What Works

1. Document every prop — undocumented props are discovered by reading source code
2. Include TypeScript types — types are the contract between component and consumer
3. Show real examples — not just `<Button variant="primary">`, but actual use cases
4. Document accessibility — keyboard, screen reader, ARIA requirements
5. Include a do's and don'ts table — prevents common misuse
6. Provide a migration guide — reduces friction on upgrades
7. Keep docs next to code — co-located docs stay in sync better than separate wikis

## Common Mistakes

1. Missing default values — developers don't know what they get if they omit a prop
2. No examples for complex props — `variant` with 5 options needs 5 examples
3. No accessibility section — components without a11y docs lead to inaccessible UIs
4. No migration guide — breaking changes without migration instructions cause frustration
5. Outdated docs — docs that don't match the code are worse than no docs
6. No testing examples — consumers don't know how to test with the component
7. Vague descriptions — "sets the color" doesn't explain what colors are valid

## Frequently Asked Questions

### How do we keep documentation in sync with code?

Co-locate docs with the component source. Use a tool like Storybook that generates docs from the component's TypeScript types and JSDoc comments. Run a CI check that verifies documented props match the actual component props. Review docs in the same PR that changes the component.

### Should we use Storybook or Markdown docs?

Both. Storybook provides interactive examples and auto-generated prop tables. Markdown provides narrative documentation, migration guides, and usage patterns. Use Storybook for reference, Markdown for guides.

### How detailed should prop descriptions be?

Detailed enough that a developer can use the prop without reading the source code. Include valid values, default behavior, and interaction with other props. For example, `loading` should mention that it disables interaction and shows a spinner.

### What about internal props or methods?

Don't document them. If a prop is internal (prefixed with `_` or marked `@internal`), exclude it from public docs. Documenting internal APIs creates an implicit contract that makes refactoring harder.

### How do we document polymorphic components?

Document the `as` prop with all valid values. For each value, document the additional props that become available (e.g., `href` when `as="a"`). Show examples for each polymorphic variant. TypeScript discriminated unions help enforce correct usage.