skip to content

Props & Variants API

Shaping a shared element's public inputs: enum variants over boolean flags, controlled and uncontrolled use, polymorphic render targets and passthrough. Each choice becomes a breaking change.

part ofDesign systems & UX foundationsoverview, primer and where to startread it →
on this pageshow

questions

4

In a shared component library, why do teams give a component one enum variant property instead of several boolean style flags?

level: middleimportance: must knowfreq 58%

answer

  1. count the combinations
  2. which flag wins?
  3. mutually exclusive by construction
  4. adding a value vs adding a flag
  5. booleans for independent axes only

basics

~20 s

Several boolean style flags let callers ask for combinations that make no sense, with the winner hidden in the implementation. One enum variant property makes the options mutually exclusive, exhaustively checkable and easy to extend; booleans stay for genuinely independent axes.

solid answer

~40 s

With three flags such as primary, destructive and subtle, a button has eight expressible combinations and maybe four meaningful ones. Someone will pass primary and destructive together, and which one wins is decided by an accident of the implementation that nobody documented. A single `variant` property with the values primary, secondary, destructive and subtle makes the choice **mutually exclusive by construction**, lets the type checker reject a value that does not exist, gives one obvious default, and lines up with how design editors model variants. Adding a new value later is additive; adding a new flag multiplies the combinations again. Booleans are still right for an **independent axis** that combines validly with everything else, such as full width. My rule: if two flags cannot both be true, they are one enum.

code

pseudocode · 8 lines
pseudocode
// three style flags: 8 expressible states, about 4 meaningful
ActionButton(label: "Cancel trip", isPrimary: true, isDestructive: true)  // which wins?

// one enum axis plus an independent boolean axis
ActionButton(label: "Cancel trip", variant: destructive)
ActionButton(label: "Accept trip", variant: primary, fullWidth: true)

variant := primary | secondary | destructive | subtle   // default: secondary

go deeper

for a junior

Recall that one variant property with named values replaces a pile of style flags, and that callers pick exactly one value.

for a middle

Explain the combinatorial problem, hidden precedence and why an enum is additive to extend, and name when a boolean is still correct: an independent axis.

for a senior

Show how you would migrate shipped flags to an enum without breaking callers, and how you decide between one flat enum and several axes.

for a principal

Treat every property name as a long-lived public contract: set API review rules for the whole library so style choices are modelled consistently across components and platforms.

## The problem with style flags Imagine the action button in a ride-hailing driver app's shared component library. It started with an `isPrimary` flag. Then "Cancel trip" needed a red button, so `isDestructive` arrived; then a low-emphasis "Details" link-style action added `isSubtle`. Each flag was a small, reasonable change. Together they produce an API with these properties: - **Combinatorial surface.** Three booleans give 2 × 2 × 2 = 8 expressible states. Perhaps four are meaningful. The other four still compile, render something, and end up in production. - **Hidden precedence.** When a caller passes primary *and* destructive, one wins — whichever style rule or branch happens to come last. That precedence is not a design decision; it is an accident nobody documented and nobody can safely change. - **Double negatives and noise.** Call sites fill with explicit false values, and readers have to reconstruct which single state the flags add up to. - **Growth multiplies.** A fourth variant means a fourth flag and sixteen combinations, and each new flag is one more public name that can never be removed casually. ## What an enum variant fixes Replace the flags with one property whose value is chosen from a closed set: `variant` = primary | secondary | destructive | subtle. | Concern | Boolean flags | One enum variant | |---|---|---| | Invalid combinations | expressible, render silently | impossible to express | | Precedence | implicit, undocumented | none needed: one value applies | | Default | each flag defaults separately | one documented default value | | Type checking | any mix passes | an unknown value is rejected | | Adding an option | new flag, combinations double | new value, additive | | Design parity | no single place to compare | matches a design editor's variant property | The enum also makes the style mapping a **lookup table**: each value maps to one set of semantic style values, which is easier to test and to review than a chain of conditionals. ## When booleans are still right The argument is against flags that are really *one choice spread over several properties*, not against booleans: 1. **Independent axes** — full width, or showing a leading icon — combine validly with every variant, so a boolean is honest. 2. **Genuine two-value settings that will never grow** — rare, and worth questioning. "Only two styles today" is how the first flag of a future enum is born. 3. **Several enums for several axes.** When combinations *are* all valid, split them: an `emphasis` axis (solid, outline, ghost) and a `tone` axis (neutral, danger) give six valid combinations without a flat list of six names. The test is the same: every combination the API allows must be one the design supports. The practical rule: **if two flags cannot both be true, they are one enum.** ## Naming the values An enum is only as good as its value names. Two habits keep it durable: - **Name by intent, not appearance.** `destructive` survives a rebrand; `red` does not, and it becomes a lie in a theme where destructive actions are drawn differently. - **Keep one vocabulary across components.** If the button says `destructive`, the banner and the confirmation chip should not say `danger` and `critical`. Callers learn one set of words, and the style lookup tables stay parallel. - **Pick the default deliberately.** The default value is the one most call sites get without thinking, so it should be the least emphatic option that is still correct in most places. ## Migrating from flags A library that already shipped flags does not flip overnight. The usual path is to add the enum, map the old flags onto it with the documented precedence, warn at development time when flags are used, and remove them in a later major release. How that removal is classified and scheduled belongs to the library's versioning policy; the API lesson is simply that each flag you ship is a name you will have to retire. ## Beyond the web The same shape appears on every platform a design system serves. A native mobile component library exposes a style enum rather than a set of flags for the same reasons, and a design editor's component variants are a closed set of named options. When the coded API and the design asset both model "one of N", parity checks between them become a simple comparison of two lists.

  • When a component has two style axes whose combinations are all valid, should the library use one flat enum or two enums?
    Two enums, one per axis, when every combination is a supported design: emphasis and tone multiply cleanly and each stays small. A flat enum of every pairing grows as the product of the axes and hides that structure. If only some pairings are supported, a flat enum of the valid ones is safer, because two enums would expose the unsupported combinations again.
  • A library already ships three boolean style flags; how do you move callers to an enum variant?
    Add the enum alongside the flags, translate flags to enum values inside the component with the precedence written down, and warn in development whenever a flag is used. Once call sites have moved, remove the flags in a major release under the library's deprecation policy. Never change which flag wins during the transition, because callers depend on the old accident.
  • Why is 'only two styles today' a weak reason for a boolean style property?
    Style sets grow. When a third style arrives, a boolean cannot express it, so a second flag is added and invalid combinations appear. Starting with a two-value enum costs nothing and lets the third value arrive as an addition rather than a redesign.

A car's gear selector: one lever with park, reverse, neutral and drive cannot be in reverse and drive at once, whereas a separate switch for each gear could be — and someone would eventually flip two.

saying these in an interview costs you the question

  • Boolean flags are more flexible, so they are the better long-term API
  • Invalid flag combinations are the caller's problem, so the library need not prevent them
  • Every boolean property in a component library is a design smell
  • Adding a new enum value is always a breaking change for callers
  • Two styles today justifies a boolean because a third will never come
  • Enums are only about runtime performance, not about API correctness
open as a page

In a shared component library, how should a stateful component let callers either own its state or leave it to the component?

level: middleimportance: should knowfreq 48%

basics

~20 s

Offer both modes through one convention: a value input makes the component controlled, a default-value input seeds internal state for uncontrolled use, and a change callback fires in either mode. The mode is fixed at first render.

open as a page

In a shared component library, a driver-app team used a button's polymorphic render-target property to render it as a generic container; what broke, and how should the API prevent it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The styled container kept the button's look and pointer tap but lost its role, focusability and keyboard activation, so assistive-technology and keyboard users could not accept trips. The API should restrict render targets to semantically compatible ones and type the properties each allows.

open as a page

For a shared component library used by thirty product teams, how open should the API be to passthrough attributes, host references and style overrides?

level: principalimportance: should knowfreq 30%

basics

~20 s

Open enough that teams never fork, closed enough that internals stay changeable. Most libraries curate: attributes pass to one documented element, a host reference points at the primary focusable node, style changes go through named override points, and escape-hatch usage is measured.

open as a page