Learn Prop Naming

66 patterns across 12 categories. Each one shows the convention, a side-by-side example, and why it matters.

Boolean Props
5 patterns

Using is, has, should, and other prefixes to make yes/no props obvious at a glance.

Avoid
interface ButtonProps {
  text: string;
  loading: boolean;
}

Prefer
interface ButtonProps {
  text: string;
  isLoading: boolean;
}
Callback Naming
7 patterns

Why the on prefix matters and how to name event handler props so they read like English.

Avoid
interface SliderProps {
  value: number;
  setValue: (v: number) => void;
}

Prefer
interface SliderProps {
  value: number;
  onValueChange: (value: number) => void;
}
Default Values
5 patterns

Setting sensible defaults via destructuring, stable references, and default callbacks.

Avoid
interface BadgeProps {
  label: string;
  color?: string;
  size?: string;
}

Badge.defaultProps = {
  color: 'gray',
  size: 'md',
};

Prefer
interface BadgeProps {
  label: string;
  /** @default "gray" */
  color?: 'gray' | 'blue' | 'green' | 'red';
  /** @default "md" */
  size?: 'sm' | 'md' | 'lg';
}

function Badge({
  label,
  color = 'gray',
  size = 'md',
}: BadgeProps) {
Prop Specificity
7 patterns

Replacing vague primitives with union types, template literals, and accessible text props.

Avoid
enum ButtonVariant {
  Primary = 'primary',
  Secondary = 'secondary',
  Danger = 'danger',
}

interface ButtonProps {
  label: string;
  variant: ButtonVariant;
}

Prefer
interface ButtonProps {
  label: string;
  variant: 'primary' | 'secondary' | 'danger';
}
JSDoc
5 patterns

Documenting defaults, examples, and deprecations so consumers don't have to read your source.

Avoid
interface BadgeProps {
  label: string;
  variant: 'default' | 'success' | 'warning';
}

Prefer
interface BadgeProps {
  /** The text displayed inside the badge. */
  label: string;
  /**
   * The visual style variant of the badge.
   * @default 'default'
   */
  variant: 'default' | 'success' | 'warning';
}
Prop Organization
6 patterns

Grouping related props, removing redundancy, and knowing when to extract sub-components.

Avoid
interface ProfileCardProps {
  userName: string;
  userEmail: string;
  userAvatar: string;
  userRole: 'admin' | 'member' | 'viewer';
  onEdit: () => void;
}

Prefer
interface User {
  name: string;
  email: string;
  avatar: string;
  role: 'admin' | 'member' | 'viewer';
}

interface ProfileCardProps {
  user: User;
  onEdit: () => void;
}
Children Pattern
6 patterns

Composition, compound components, named slots, and the slots/slotProps convention.

Avoid
interface ListProps<T> {
  /** The data to iterate. */
  data: T[];
  /** Render function for each item. */
  render: (item: T) => React.ReactNode;
  /** Empty state content. */
  empty: React.ReactNode;
}

Prefer
interface ListProps<T> {
  /** The data array to iterate over. */
  data: T[];
  /**
   * Render function called for each item.
   * Receives the item and its index.
   */
  renderItem: (item: T, index: number) => React.ReactNode;
  /**
   * Content shown when the data array is empty.
   * @example
   * emptyState={<EmptyMessage text="No items" />}
   */
  emptyState: React.ReactNode;
}
Render Props
5 patterns

When to use render functions vs ReactNode slots, and how to name headless component APIs.

Avoid
interface TooltipProps {
  children: React.ReactNode;
  content: React.ReactNode;
}

// Usage:
// <Tooltip content={<span>{data.label}</span>}>
//   <Button />
// </Tooltip>

Prefer
interface TooltipProps {
  children: React.ReactNode;
  /** Receives the anchor rect for positioning. */
  renderContent: (anchorRect: DOMRect) => React.ReactNode;
}

// Usage:
// <Tooltip renderContent={(rect) => (
//   <Popover style={{ top: rect.bottom }}>
//     {data.label}
//   </Popover>
// )}>
//   <Button />
// </Tooltip>
Extending HTML
5 patterns

Forwarding native HTML attributes with ComponentProps, Omit, and polymorphic as props.

Avoid
interface ButtonProps
  extends React.ComponentProps<'button'> {
  /** The button text. */
  label: string;
  /** Click handler. */
  onClick?: () => void;
  /** Whether the button is disabled. */
  disabled?: boolean;
  /** Button type. @default 'button' */
  type?: 'button' | 'submit' | 'reset';
}

Prefer
interface ButtonProps
  extends React.ComponentProps<'button'> {
  /** The button's visible text content. */
  label: string;
  /** Visual style variant. @default 'primary' */
  variant?: 'primary' | 'secondary' | 'danger';
  /** Button size. @default 'md' */
  size?: 'sm' | 'md' | 'lg';
}
Ref Forwarding
5 patterns

Forwarding refs to the right element, typing them correctly, and keeping imperative handles minimal.

Avoid
interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
  onClick?: () => void;
}

Prefer
interface ButtonProps
  extends React.ComponentProps<'button'> {
  variant?: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}
Accessibility Props
5 patterns

Making aria-label required, typing ARIA roles as unions, and wiring up labelledby/describedby.

Avoid
interface IconButtonProps
  extends React.ComponentProps<'button'> {
  /** The icon to display. */
  icon: React.ReactNode;
  /** Visual style variant. @default 'default' */
  variant?: 'default' | 'outlined';
  /** Button size. @default 'md' */
  size?: 'sm' | 'md' | 'lg';
}

Prefer
interface IconButtonProps
  extends React.ComponentProps<'button'> {
  /** The icon to display. */
  icon: React.ReactNode;
  /** Accessible label for screen readers. */
  'aria-label': string;
  /** Visual style variant. @default 'default' */
  variant?: 'default' | 'outlined';
  /** Button size. @default 'md' */
  size?: 'sm' | 'md' | 'lg';
}
Discriminated Unions
5 patterns

Modeling variant-dependent props with TypeScript so impossible states are unrepresentable.

Avoid
interface StatusProps {
  status: 'loading' | 'error' | 'success';
  errorMessage?: string;
  successData?: string;
}

Prefer
type StatusProps =
  | { status: 'loading' }
  | { status: 'error'; errorMessage: string }
  | { status: 'success'; data: string };