12/6/2026 · 13 min read
This article is about the structural decisions — in both Figma and code — that determine whether a component system stays coherent as it grows. Props API design, composition vs configuration, variant architecture, and versioning strategy. The patterns here come from design systems that have survived multi-year product evolution without requiring a full rewrite.
The most important architectural principle: prefer composable primitives over configured monoliths. Instead of one complex component, build small focused components that compose cleanly.
// ❌ Monolith: one component, infinite props
<Button
variant="primary"
size="lg"
leftIcon="search"
rightIcon="chevron"
isLoading={loading}
loadingText="Searching..."
isDisabled={disabled}
fullWidth={true}
tooltip="Search for items"
onClick={handleClick}
/>
// 47 props and counting. What are valid combinations?
// What happens when loading=true AND disabled=true?
// ✅ Composition: focused primitives that assemble
<Button variant="primary" size="lg" onClick={handleClick}>
<Icon name="search" />
Search
<Icon name="chevron-right" />
</Button>
// Loading state is a wrapper concern:
<LoadingOverlay isLoading={loading}>
<Button variant="primary">Search</Button>
</LoadingOverlay>
// Or a slot-based composition:
<Button variant="primary">
{loading ? <Spinner size="sm" /> : 'Search'}
</Button>
Composition keeps each component's responsibility small and its props surface minimal. The component doesn't need to know about loading states, tooltips or icons — those are composition concerns, not Button concerns.
The props API is the public interface of your component. Once it's in production, changing it is a breaking change. Design it deliberately.
// ❌ Presentational: ties API to visual implementation
<Button color="blue" size="40px" borderRadius="8px" />
// ✅ Semantic: describes intent, not appearance
<Button variant="primary" size="md" />
// Why: if the design system changes "primary" from blue to red,
// no consuming team needs to change code. The mapping is internal.
// ❌ Implicit: magic string with undocumented valid values
<Badge type="success" /> // What other types exist? Nobody knows.
// ✅ Explicit: typed union, self-documenting
type BadgeVariant = 'success' | 'warning' | 'error' | 'info' | 'neutral';
<Badge variant="success" />
Buttons need to render as <a> tags sometimes. List items might be <div> or <li>. The polymorphic pattern handles this without duplication:
// The 'as' prop changes the rendered element
<Button as="a" href="/contact" variant="primary">
Contact us
</Button>
// Renders: <a href="/contact" class="button button--primary">Contact us</a>
// TypeScript ensures the correct props for each element:
function Button<T extends ElementType = 'button'>({
as,
...props
}: ButtonProps<T>) {
const Component = as || 'button';
return <Component {...props} />;
}
Design-code drift is the silent killer of design systems. The solution is designing Figma components with the same architectural principles as code components.
Every Figma component should have a direct code equivalent with matching props. If a Figma variant doesn't exist in code, it should be removed from Figma (or added to code). The design system backlog should be the single source of truth for what's canonical.
Figma variants map directly to component props. A Button with variants variant/primary, secondary, ghost and size/sm, md, lg corresponds exactly to:
type ButtonVariant = 'primary' | 'secondary' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant: ButtonVariant;
size: ButtonSize;
// ... other props
}
Figma's boolean variants (Disabled: true/false, Loading: true/false) should map to boolean props in code. Not variant="disabled" — that conflates variant with state. Disabled is a state that overlays any variant.
// ❌ State as variant
<Button variant="primary-disabled" />
// 3 variants × 2 states = 6 combinations. Add another state: 12. Unmaintainable.
// ✅ State as separate prop
<Button variant="primary" disabled />
// 3 variants + n states = 3 + n. Scales cleanly.
In Figma, use nested components as slots. A Card component has a Header slot and a Body slot — each is itself a component. This mirrors the composition pattern in code and prevents the Figma component from becoming a hardcoded monolith.
A design system without versioning is a liability. Every update is a potential breaking change for every team using it.
// Step 1: Mark deprecated in code (one major version warning period)
interface ButtonProps {
/** @deprecated Use `variant` instead. Will be removed in v4. */
type?: 'primary' | 'secondary';
variant?: 'primary' | 'secondary';
}
// Step 2: Runtime warning in development
if (props.type && process.env.NODE_ENV === 'development') {
console.warn('Button: `type` prop is deprecated. Use `variant` instead.');
}
// Step 3: In Figma — mark deprecated variants with 🚫 prefix
// 🚫 type/primary → replaced by variant/primary
// Step 4: Remove in next major version with migration guide
Every release needs a changelog entry readable by both designers and developers. Not just "fixed button bug" — but what changed, why, and what consuming teams need to do. The changelog is often the most-read document in a design system.
Design system components need two kinds of tests: functional tests (does it work?) and visual regression tests (does it look right?).
axe-core against every component story in CI. Accessibility regressions are the most expensive to fix post-launch and the easiest to catch pre-merge.// Storybook story with all meaningful states
export const AllButtonVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{(['primary', 'secondary', 'ghost'] as const).map(variant =>
(['sm', 'md', 'lg'] as const).map(size => (
<Button key={`${variant}-${size}`} variant={variant} size={size}>
{variant} {size}
</Button>
))
)}
</div>
)
};