| name | component-library-architecture |
| description | Structuring a cross-platform component library into primitives, patterns, and compositions with predictable folder layout and public API. Use this when starting or reorganizing a component library. |
Component Library Architecture
Instructions
A mobile component library succeeds when its boundaries are obvious. Three tiers, one public surface per tier, and a strict rule that higher tiers compose lower ones — never the reverse.
1. Three Tiers
| Tier | Responsibility | Examples |
|---|
| Primitives | Atomic, single-purpose, style-driven by tokens only. | Text, Icon, Button, Surface |
| Patterns | Opinionated combinations of primitives. | Field, ListItem, SearchBar |
| Compositions | Full UI moments, often domain-aware. | EmptyState, Form, AppBar |
Rule: primitives never import patterns; patterns never import compositions.
2. Canonical Folder Layout
Each platform mirrors the same structure.
design-system/
├── tokens/
│ ├── reference/
│ ├── system/
│ └── component/
└── src/
├── primitives/
│ ├── Button/
│ │ ├── Button.kt # or .swift / .dart / .tsx
│ │ ├── ButtonTokens.kt
│ │ ├── ButtonVariants.kt
│ │ └── Button.snapshot.kt
│ └── Text/
├── patterns/
│ └── Field/
└── compositions/
└── EmptyState/
3. One Public Entry Point
Each component has exactly one exported symbol (the component) and one exported type set (its props / state enums). Internals (*Tokens, *Internal, styled children) stay package-private.
Compose:
@Composable
fun Button(
onClick: () -> Unit,
label: String,
modifier: Modifier = Modifier,
size: ButtonSize = ButtonSize.Medium,
intent: ButtonIntent = ButtonIntent.Primary,
leadingIcon: @Composable (() -> Unit)? = null,
)
internal object ButtonTokens { }
SwiftUI:
public struct DSButton: View {
public init(_ label: String, intent: DSButtonIntent = .primary,
size: DSButtonSize = .medium, action: @escaping () -> Void) { ... }
public var body: some View { ... }
}
Flutter:
class DsButton extends StatelessWidget {
const DsButton({super.key, required this.label, required this.onPressed,
this.intent = DsButtonIntent.primary, this.size = DsButtonSize.medium});
final String label;
final VoidCallback onPressed;
final DsButtonIntent intent;
final DsButtonSize size;
}
React Native:
export type ButtonProps = { label: string; intent?: ButtonIntent; size?: ButtonSize; onPress: () => void };
export const Button: React.FC<ButtonProps> = ({ ... }) => { ... };
4. Component File Contract
Every component folder ships six things:
- The component source.
- A tokens file that resolves design tokens into component-scoped values.
- A variants file with enums/sealed classes for every axis (size, intent, state).
- Unit tests (variant prop plumbing, event handlers).
- A snapshot / golden test per variant × theme.
- A docs page or Storybook story.
5. Public API Stability
- Props are additive. Removing a prop is a major bump.
- Enums are closed — exhaustive
when / switch must still compile for consumers.
- Default values are part of the contract. Changing a default is a minor bump with a changelog entry.
6. Dependency Direction
compositions/ ──► patterns/ ──► primitives/ ──► tokens/
Enforce via module boundaries (Gradle modules on Android, Swift package targets, Dart packages, TS path mappings). A CI lint fails builds on backward edges.
7. Anti-Patterns
- One giant component (
<Card> that is also a list item, a hero, and a modal).
- Primitives that import domain types (
User, OrderStatus).
- Parallel component variants (
ButtonPrimary, ButtonDanger) instead of one component with variant props.
- Exporting internal tokens publicly, which locks you out of refactors.
Checklist