Skip to main content
Saved

Props and Attributes

Category Components

Level Beginner

Examples

React, Vue, Svelte, Native web

Pass data and configuration into components through properties or attributes.

In this article
  1. Problem
  2. Solution
  3. Example
  4. Basic Props
  5. Props Spreading
  6. TypeScript Props
  7. Props with Children
  8. Benefits
  9. Tradeoffs
  10. Summary
  11. See also

Props and Attributes

Problem

You need Submit, Cancel, and Delete buttons that look identical except for the label, but without props, you’re stuck copying nearly identical component files, changing one word, and repeating everywhere.

I’ve seen codebases with SubmitButton.tsx, CancelButton.tsx, DeleteButton.tsx, SaveButton.tsx, UpdateButton.tsx, ConfirmButton.tsx… you get the idea. They all do the same thing with different hardcoded text.

Testing gets hard too. You can’t inject test data because the component fetches its own, and simulating error states is nearly impossible when everything is hardcoded inside the component.

Solution

Props let you pass data in from the outside instead of hardcoding it inside. A Button component accepts a label prop, so passing label="Submit" or label="Cancel" gives you one component to maintain instead of twenty.

Props can be anything: strings, numbers, objects, arrays, functions. You can pass user data, click handlers, and style variants, and the component renders accordingly.

Sensible defaults mean consumers don’t have to specify everything. A variant might default to “primary,” and disabled to false. A missing required prop should produce an error, and TypeScript makes that easy by flagging it at compile time.

Web components have two separate mechanisms: HTML attributes (always strings) and JavaScript properties (any type). Attributes work in declarative HTML; properties are for the JavaScript API.

Example

This Button component handles different labels, variants, and sizes, all through props.

Basic Props

function Button({ label, onClick, disabled = false, variant = 'primary', size = 'medium', icon = null }) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled}
    >
      {icon && <span className="btn-icon">{icon}</span>}
      {label}
    </button>
  );
}

<Button label="Submit" onClick={handleSubmit} />
<Button label="Cancel" onClick={handleCancel} variant="secondary" />
<Button label="Delete" onClick={handleDelete} variant="danger" disabled={loading} />

Props Spreading

function Card({ title, children, className, ...rest }) {
  return (
    <div className={`card ${className || ''}`} {...rest}>
      <h2>{title}</h2>
      <div className="card-content">{children}</div>
    </div>
  );
}

<Card title="User Profile" className="profile-card" data-testid="profile-card">
  <UserDetails />
</Card>

TypeScript Props

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
}

function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

Props with Children

function Card({ title, footer, children }) {
  return (
    <div className="card">
      {title && <div className="card-header"><h3>{title}</h3></div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

<Card title="User Profile" footer={<Button label="Save" />}>
  <UserForm />
</Card>

Benefits

  • One component covers many uses. The same Button works for “Submit,” “Cancel,” and “Delete” with a prop change.
  • Testing gets easier. You pass mock data as props and check the output, without internal state getting in the way.
  • Components become reusable across projects. A well-designed Button component works anywhere.
  • Props suit Storybook well, since you can document every variant by passing different prop combinations.
  • TypeScript catches mistakes at compile time. If you pass the wrong type to a prop, your editor flags it before the code runs.
  • Logic stays centralized. Fix a bug once in the component, and all consumers benefit.

Tradeoffs

  • Prop explosion is a real problem. With enough configuration options you end up with components that take twenty props, and the API gets hard to use.
  • Conditional logic piles up, because most new props add another if statement inside the component.
  • Prop drilling gets tedious. Passing data through five layers of components to reach a deeply nested child is annoying and error-prone.
  • Default values can hide bugs. If someone passes undefined and it silently falls back to a default, you might not notice the upstream problem.
  • Boolean props read awkwardly. disabled={false} is harder to read than omitting the prop.
  • Spreading props ({...rest}) loses type safety. You don’t know what’s being passed through.
  • Web component attributes only support strings. Complex data needs JSON serialization or separate property setters.
  • The more optional props you have, the more combinations you need to test, because each new one multiplies the number of combinations.

Summary

Props turn a single component into a building block that serves multiple use cases through configuration rather than duplication. Passing data, callbacks, and styling variants as props gives you components that make no assumptions about where their data comes from. Sensible defaults for optional props and TypeScript types keep the component API clear and predictable.

See also

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.