Learn Prop Naming
66 patterns across 12 categories. Each one shows the convention, a side-by-side example, and why it matters.
Boolean Props
Using is, has, should, and other prefixes to make yes/no props obvious at a glance.
interface ButtonProps {
text: string;
loading: boolean;
}interface ButtonProps {
text: string;
isLoading: boolean;
}Callback Naming
Why the on prefix matters and how to name event handler props so they read like English.
interface SliderProps {
value: number;
setValue: (v: number) => void;
}interface SliderProps {
value: number;
onValueChange: (value: number) => void;
}Default Values
Setting sensible defaults via destructuring, stable references, and default callbacks.
interface BadgeProps {
label: string;
color?: string;
size?: string;
}
Badge.defaultProps = {
color: 'gray',
size: 'md',
};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
Replacing vague primitives with union types, template literals, and accessible text props.
enum ButtonVariant {
Primary = 'primary',
Secondary = 'secondary',
Danger = 'danger',
}
interface ButtonProps {
label: string;
variant: ButtonVariant;
}interface ButtonProps {
label: string;
variant: 'primary' | 'secondary' | 'danger';
}JSDoc
Documenting defaults, examples, and deprecations so consumers don't have to read your source.
interface BadgeProps {
label: string;
variant: 'default' | 'success' | 'warning';
}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
Grouping related props, removing redundancy, and knowing when to extract sub-components.
interface ProfileCardProps {
userName: string;
userEmail: string;
userAvatar: string;
userRole: 'admin' | 'member' | 'viewer';
onEdit: () => void;
}interface User {
name: string;
email: string;
avatar: string;
role: 'admin' | 'member' | 'viewer';
}
interface ProfileCardProps {
user: User;
onEdit: () => void;
}Children Pattern
Composition, compound components, named slots, and the slots/slotProps convention.
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;
}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
When to use render functions vs ReactNode slots, and how to name headless component APIs.
interface TooltipProps {
children: React.ReactNode;
content: React.ReactNode;
}
// Usage:
// <Tooltip content={<span>{data.label}</span>}>
// <Button />
// </Tooltip>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
Forwarding native HTML attributes with ComponentProps, Omit, and polymorphic as props.
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';
}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
Forwarding refs to the right element, typing them correctly, and keeping imperative handles minimal.
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
onClick?: () => void;
}interface ButtonProps
extends React.ComponentProps<'button'> {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}Accessibility Props
Making aria-label required, typing ARIA roles as unions, and wiring up labelledby/describedby.
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';
}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
Modeling variant-dependent props with TypeScript so impossible states are unrepresentable.
interface StatusProps {
status: 'loading' | 'error' | 'success';
errorMessage?: string;
successData?: string;
}type StatusProps =
| { status: 'loading' }
| { status: 'error'; errorMessage: string }
| { status: 'success'; data: string };