StackPractices
intermediate Por Mathias Paulenko

Plantilla de Documentación de Component API

Una plantilla para documentar APIs de componentes UI: props, events, slots, methods, accessibility y usage examples con TypeScript types.

Temas: testing

Overview

Component API documentation le dice a developers cómo usar un component: qué props accept, qué events emite, qué slots provee y qué methods expone. Sin clear API docs, developers leen source code para entender components, wasteando time y leading a incorrect usage.

When to Use

  • For alternatives, see Browser Support Matrix Template.

  • Documentando design system components

  • Publicando un component library

  • Onboardéando new developers a un component system

  • Creando component usage guidelines

  • Manteniendo 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

Un versatile button component con support para multiple variants, sizes, icons, loading states y full keyboard navigation. Renderea como un native `<button>` element por default, con polymorphic rendering via el `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
disabledbooleanfalseNoDisablea interaction y apply disabled styles
loadingbooleanfalseNoMuestra loading spinner y disablea interaction
fullWidthbooleanfalseNoHace que button take full width de container
iconReactNodeundefinedNoIcon element rendereado before children
iconPosition'left' | 'right''left'NoPosition de icon relative a text
type'button' | 'submit' | 'reset''button'NoNative button type attribute
as'button' | 'a' | React.ElementType'button'NoPolymorphic rendering element
hrefstringundefinedNoURL cuando as="a"; required para link rendering
targetstringundefinedNoLink target attribute cuando as="a"
relstringundefinedNoLink rel attribute cuando as="a"
ariaLabelstringundefinedNoAccessible label para icon-only buttons
testIdstringundefinedNoData attribute para 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

Control el visual style del button. Cada variant tiene distinct colors para default, hover, active y disabled states.

VariantUse CaseDefault BGText ColorBorder
primaryMain action en un 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

Control el height, padding y font size del 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

Cuando true, el button muestra un spinner, disablea interaction y maintain su width para prevenir layout shift.

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

as (Polymorphic)

Renderea el button como un different element mientras maintain styling. Common use: renderear como un anchor para 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
onClickMouseEventFire cuando button se clickea (no cuando disabled o loading)
onFocusFocusEventFire cuando button recibe focus
onBlurFocusEventFire cuando button pierde 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 o content
iconundefinedIcon rendereado before o after children basado en iconPosition

Slot Examples

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

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

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

// Icon only (require 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

El Button component no expone imperative methods. All interaction se handlea through props y events.

Accessibility

AttributeValueNotes
Rolebutton (native)Usa native <button> element
KeyboardEnter/SpaceTriggerea onClick
FocusVisible outlinefocus-visible styles applied
Disabledaria-disabledSet cuando disabled o loading es true
Labelaria-labelRequired para icon-only buttons via ariaLabel prop

Accessibility Examples

// Icon-only button — ariaLabel es 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
Usá primary para el main action en un pageUsá multiple primary buttons en el same view
Proveé ariaLabel para icon-only buttonsUsá icon-only buttons sin accessible labels
Usá loading en vez de disablear + text changeCambiá button text a “Loading…” manualmente
Usá as="a" para navigationUsá onClick con window.location para navigation
Usá danger para destructive actionsUsá danger para non-destructive actions
Usá fullWidth en mobile layoutsUsá fullWidth en 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 para clarity
blockfullWidthRenamed para consistency
isLoadingloadingSimplified naming
iconLefticon + iconPosition="left"Merged en single prop
iconRighticon + iconPosition="right"Merged en single prop

## Explanation

Component API documentation sirve a two audiences: developers que usan el component y developers que lo maintain. Users necesitan saber qué props existen, qué types accept y qué son los defaults. Maintainers necesitan trackear changes, deprecations y migration paths.

La props table es el core de la documentation. Cada prop debería tener un TypeScript type, un default value, si es required y un description. Para union types (como `variant`), listé todos los valid values. Para complex props, addeá un detailed subsection con examples.

Events documentean los callback props. Cada event debería specify el payload type y cuándo fire. Notá edge cases: ¿`onClick` firea cuando el button está disabled? (No debería.)

Slots documentean dónde content puede ser projected. En React, esto es `children` y render props. En Vue, esto es named slots. En Angular, esto es `ng-content` con selectors. Documentá qué content se espera y cómo se posiciona.

La accessibility section es non-negotiable. Cada component debería documentar su keyboard interactions, ARIA attributes y required labels. Icon-only buttons deben documentar el `ariaLabel` requirement.

La testing section muestra cómo testear el component. Esto helpa a consumers a escribir integration tests que incluyan el component. También sirve como executable documentation — los tests verify el documented behavior.

El migration guide helpa con version upgrades. Listé every breaking change con el old API y el new API. Esto reduce 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 se discovered leyendo source code
2. Incluí TypeScript types — types son el contract entre component y consumer
3. Mostrá real examples — no solo `<Button variant="primary">`, sino actual use cases
4. Documentá accessibility — keyboard, screen reader, ARIA requirements
5. Incluí un do's and don'ts table — previene common misuse
6. Proveé un migration guide — reduce friction en upgrades
7. Mantené docs next a code — co-located docs stay in sync better que separate wikis

## Common Mistakes

1. Missing default values — developers no saben qué get si omiten un prop
2. No examples para complex props — `variant` con 5 options necesita 5 examples
3. No accessibility section — components sin a11y docs lead a inaccessible UIs
4. No migration guide — breaking changes sin migration instructions causan frustration
5. Outdated docs — docs que no matchean el code son worse que no docs
6. No testing examples — consumers no saben cómo testear con el component
7. Vague descriptions — "setea el color" no explica qué colors son valid

## FAQ

### ¿Cómo mantenemos documentation in sync con code?

Co-locá docs con el component source. Usá un tool como Storybook que genere docs desde el component's TypeScript types y JSDoc comments. Corré un CI check que verify que documented props matcheen los actual component props. Revieweá docs en el same PR que cambia el component.

### ¿Deberíamos usar Storybook o Markdown docs?

Ambos. Storybook provee interactive examples y auto-generated prop tables. Markdown provee narrative documentation, migration guides y usage patterns. Usá Storybook para reference, Markdown para guides.

### ¿Qué tan detailed deberían ser prop descriptions?

Detailed enough que un developer pueda usar el prop sin leer el source code. Incluí valid values, default behavior y interaction con otros props. Por ejemplo, `loading` debería mention que disablea interaction y muestra un spinner.

### ¿Qué hay de internal props o methods?

No los documentes. Si un prop es internal (prefixed con `_` o marked `@internal`), excluílo de public docs. Documentar internal APIs crea un implicit contract que hace refactoring harder.

### ¿Cómo documentamos polymorphic components?

Documentá el `as` prop con all valid values. Para cada value, documentá los additional props que become available (e.g., `href` cuando `as="a"`). Mostrá examples para cada polymorphic variant. TypeScript discriminated unions help a enforce correct usage.

Preguntas frecuentes

¿Cómo mantenemos documentation in sync con code?

Co-locá docs con el component source. Usá un tool como Storybook que genere docs desde el component's TypeScript types y JSDoc comments. Corré un CI check que verify que documented props matcheen los actual component props. Revieweá docs en el same PR que cambia el component.

¿Deberíamos usar Storybook o Markdown docs?

Ambos. Storybook provee interactive examples y auto-generated prop tables. Markdown provee narrative documentation, migration guides y usage patterns. Usá Storybook para reference, Markdown para guides.

¿Qué tan detailed deberían ser prop descriptions?

Detailed enough que un developer pueda usar el prop sin leer el source code. Incluí valid values, default behavior y interaction con otros props. Por ejemplo, loading debería mention que disablea interaction y muestra un spinner.

¿Qué hay de internal props o methods?

No los documentes. Si un prop es internal (prefixed con _ o marked @internal), excluílo de public docs. Documentar internal APIs crea un implicit contract que hace refactoring harder.

¿Cómo documentamos polymorphic components?

Documentá el as prop con all valid values. Para cada value, documentá los additional props que become available (e.g., href cuando as="a"). Mostrá examples para cada polymorphic variant. TypeScript discriminated unions help a enforce correct usage.