Skip to main content

Command Palette

Search for a command to run...

Avoid the Variant Prop

Let's Export Better Components

Updated
5 min read
Avoid the Variant Prop

Developing great contracts is one of the most crucial parts of software engineering. The components exported from your design system are no exception. In a React application, this means designing thoughtful and opinionated component APIs. Yet, there’s one prop that has quietly become the default—despite being a poor abstraction: the variant prop.

You’ll also see it as type, size, or kind. It's a simple enum-style prop that lets consumers select from a predefined set of styles or behaviors. But here's the catch: while it seems harmless, it often leads to deeply flawed component design. Worse, it pushes responsibility onto the application layer—right where consistency is hardest to enforce.

What’s the Problem With variant Props?

They seem like a great idea at first. After all, enums are simple! Consider this typical usage:

<Text size="big">Hello, world</Text>

Looks fine, right? But this component doesn’t encapsulate the design rule—it defers it. It means the app must always remember that size="big" is what should be used for titles, across every context. The system isn’t enforcing a design decision; it’s offering a styling toolkit. And now, your app code is responsible for maintaining design consistency.

If your design system exports primitives like this, then the design system isn’t a system. It’s just a set of styled components. The actual design is happening in the app layer.

Design Systems Should Make Decisions, Not Defer Them

Design systems exist to encode constraints. When we expose a variant, we ask the application to choose what the design system should’ve already decided.

Let’s say we need a Thumbnail component that displays user avatars at different sizes. Instead of this:

<Thumbnail size="small" url={url} />

You could offer purpose-driven components:

<CommentAvatar url={url} />
<HeaderAvatar url={url} />

Now the design system owns the sizing logic. The application simply says what it wants to display, not how it should look. This creates a more declarative API, one that carries semantic meaning and removes styling decisions from the application layer.

God Components and Leaky Abstractions

The moment you introduce a variant prop, your component is at risk of becoming a God Component.

Let’s look at a common example:

type ButtonProps = {
  variant: 'primary' | 'secondary' | 'danger' | 'link';
  size: 'small' | 'medium' | 'large';
  isDisabled?: boolean;
  onClick?: () => void;
};

Already, this component is branching internally:

  • Only danger needs confirmation logic

  • Only link ignores size

  • Only primary and secondary share color styles

These props are not always meaningful together. This is a red flag: the component is non-cohesive. Props are only used conditionally, based on the variant. This violates the Single Responsibility Principle—a foundational idea in maintainable system design.

Like the God Class anti-pattern, this component tries to do everything. It becomes hard to reason about, harder to test, and almost impossible to extend without side effects.

A Better Approach: Small, Purposeful Components

Instead of:

<Button variant="primary" size="large">Submit</Button>

Split your design intent into meaningful components:

<PrimaryActionButton onClick={save}>Submit</PrimaryActionButton>
<DestructiveButton onClick={deleteItem}>Delete</DestructiveButton>

This removes branching, improves readability, and strengthens alignment between design and code. The app doesn’t need to know which styles to apply—it just picks the component that represents the action.

Bonus: It's Easier to Refactor

Let’s say you want to change the padding on all DestructiveButtons. With purpose-built components, you can do that in one place. With a God Component, you'd have to branch inside Button and hope no other variant breaks.

This is Open-Closed Principle in action: your system is open for extension (add a new component), but closed for modification (no need to touch shared internals).

But What About Shared Visual Consistency?

A common concern with this approach is: how do we maintain consistency across all these small components? For example, how do we ensure that all buttons still share the same border radius, font size, or padding?

The answer is: through shared design tokens or private base component primitives.

You don’t need to centralize behavior into a God Component to share styling. You can define a private BaseButton that encapsulates common styles:

// private to the component library
const BaseButton = ({ className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
  <button className={`rounded-md font-semibold ${className}`} {...props} />
);

Then you build on top of it:

const PrimaryActionButton = (props) => (
  <BaseButton className="bg-blue-600 text-white px-4 py-2" {...props} />
);

const DestructiveButton = (props) => (
  <BaseButton className="bg-red-600 text-white px-4 py-2" {...props} />
);

This lets you preserve internal consistency without sacrificing cohesion or introducing bloated variant logic.

Better yet, you can define your border radius, colors, and spacing in design tokens—either via a theme, a CSS-in-JS solution, or even utility classes like Tailwind.

That way, the system ensures consistency, while each component remains focused and readable.

Key Takeaways

  • Avoid generic variant, type, or size props in design system components

  • Expose semantic, intention-driven components that map directly to use cases

  • Watch for conditional prop relevance—it's a sign your component is doing too much

  • Lean on composition and specialization to enforce constraints in the system layer, not the app layer

  • Use base components and design tokens to coordinate shared styling without centralizing logic

  • Design APIs that describe what to show, not how it should look

Exporting flexible primitives might seem powerful—but design systems are most powerful when they encode decisions, not defer them. Let your components be opinionated. Let them enforce constraints. Let them be the system.