# Explicit Variants Over Boolean Props

**SHOULD** · **ID:** `design-explicit-variants-over-booleans` · **Category:** design
**Source:** [Composition Patterns](https://github.com/vercel-labs/agent-skills/tree/main/skills/composition-patterns)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-explicit-variants-over-booleans

> SHOULD: Model mutually-exclusive styles as one enumerated variant prop, not isPrimary/isGhost/isLarge booleans that allow illegal combinations and multiply with every new style. Keep booleans only for orthogonal toggles (disabled, loading, fullWidth). Maps cleanly onto CVA.

Model mutually-exclusive styles as one enumerated variant prop, not a pile of isPrimary/isGhost/isLarge booleans

Three boolean style props already encode eight states, most of them nonsense — `isPrimary` and `isGhost` both true has no meaning, yet the type permits it and some branch has to guess. An enumerated prop, `variant: "primary" | "secondary" | "ghost"`, makes exactly the valid states representable and the illegal ones unspellable, which is the whole point of a type. It also maps one-to-one onto class-variance-authority, the engine shadcn/ui uses to attach classes per variant, so the API and the styling share one source of truth. Keep booleans for genuinely independent, orthogonal toggles (`disabled`, `loading`, `fullWidth`) — the test is whether two of them being true at once is meaningful. If it is not, they are really one variant wearing several names.

## Rule snippet

```tsx
// good
<Button variant="ghost" size="sm" />
// bad
<Button isGhost isSmall isPrimary />
```

## Bad — do not do this

`design-explicit-variants-over-booleans-bad`

```tsx
export function ExplicitVariantsOverBooleansBad() {
  return (
    <div className="w-full max-w-md space-y-3 py-6">
      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs leading-relaxed text-foreground">
        {`<Button isPrimary isLarge />
<Button isGhost isSmall />
<Button isPrimary isGhost />  // ??? illegal combo`}
      </pre>
      <div className="flex items-center gap-2">
        <button className="rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground">Primary</button>
        <button className="rounded-lg px-3 py-1 text-xs text-muted-foreground">Ghost</button>
        <button className="rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground ring-2 ring-inset ring-destructive">??</button>
      </div>
      <p className="text-xs text-destructive">
        Boolean props multiply into illegal combinations (isPrimary + isGhost) and every new style adds another boolean to reconcile.
      </p>
    </div>
  );
}
```

## Good — do this

`design-explicit-variants-over-booleans-good`

```tsx
export function ExplicitVariantsOverBooleansGood() {
  return (
    <div className="w-full max-w-md space-y-3 py-6">
      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs leading-relaxed text-foreground">
        {`<Button variant="primary" size="lg" />
<Button variant="ghost" size="sm" />
// one variant + one size — illegal combos are unrepresentable`}
      </pre>
      <div className="flex items-center gap-2">
        <button className="rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground">Primary</button>
        <button className="rounded-lg px-3 py-1 text-xs text-muted-foreground hover:bg-muted">Ghost</button>
      </div>
      <p className="text-xs text-success">
        One enumerated variant prop makes illegal states unrepresentable and maps cleanly onto CVA — new looks are new variant values, not new booleans.
      </p>
    </div>
  );
}
```

## References

- [Vercel — composition-patterns skill](https://github.com/vercel-labs/agent-skills/tree/main/skills/composition-patterns)
- [class-variance-authority](https://cva.style/docs)
