Jugendra
All articles
React & TypeScript13 min read

Creating a reusable UI component system with React and TypeScript

A grounded approach to reusable components: stable primitives, typed variants, accessible behavior and clear limits on abstraction.

Modular interface controls connected into a coherent reusable component system

Start with the problems a system should remove

A component system is valuable when the same decisions are being made repeatedly: button height, input labeling, card borders, loading behavior and focus treatment. Reuse should reduce inconsistency and make product work faster. It should not turn a small application into a framework project.

I begin with an inventory of repeated interface patterns and the differences that are intentional. Two buttons may look similar but have different semantic roles; two cards may share a surface but contain unrelated layouts. That inventory tells me whether I need a shared component, a token, or simply consistent CSS.

Build tokens and primitives before complex patterns

Tokens give components a shared visual language: color roles, spacing, typography, radius and border treatment. I name them by purpose where possible—surface, text-muted, focus—so a palette can evolve without rewriting every component. The set stays small enough that developers can remember it.

Primitives such as Button, Input, Text and Surface should have predictable behavior and minimal assumptions about layout. They establish focus styles, disabled states and semantic defaults once. More complex components can compose them without reaching into their internal markup.

Use TypeScript to make the intended API obvious

A useful component API makes common usage easy and invalid combinations difficult. Literal unions work well for a small set of variants and sizes. Native element props should usually remain available so aria attributes, event handlers and form behavior are not accidentally blocked.

I avoid boolean combinations such as primary, secondary, compact and danger when they can create contradictory states. A variant prop communicates one decision more clearly. Types are not a replacement for documentation, but good autocomplete can show the supported path before someone needs to read the implementation.

  • Prefer variant='primary' over several competing style booleans
  • Extend the relevant native HTML attributes
  • Keep required content explicit in the prop type
  • Use discriminated unions when variants require different props

Prefer composition over an endless prop list

A card component becomes hard to use when it accepts props for every possible heading, badge, image, action and alignment. Composition lets the caller supply meaningful children while the shared component owns the surface, spacing contract and accessibility behavior.

I use slots or small related subcomponents when structure matters, but I stop before the API becomes its own language. The best boundary is usually the one that allows product teams to create new arrangements without copying the behavior that must stay consistent.

Accessibility and states belong inside the system

Reusable components multiply both good decisions and mistakes. A field should connect its label, description and error message correctly every time. A dialog needs focus management and a dependable close path. A button needs a visible focus state and a real disabled or loading behavior, not only a faded color.

I test primitives with keyboard navigation and screen-reader-friendly names before visual variants grow around them. Hover is treated as an enhancement, not the only place where information appears. Reduced-motion behavior and sufficient contrast are part of the component definition rather than tasks left for each page.

Let real usage decide what becomes reusable

I am comfortable duplicating a small amount of markup until the shared pattern is clear. Extracting too early often produces a generic component that accepts dozens of escape hatches. Once two or three real uses reveal the stable core, the abstraction becomes easier to name and test.

When a component changes, I review its consumers rather than assuming visual consistency means behavioral compatibility. A small usage page or component catalogue can document variants and edge cases without introducing a large toolchain. The system stays useful when deleting or splitting a component is allowed as the product learns.

  • Abstract after the repeated responsibility is understood
  • Document supported variants and edge cases
  • Review all consumers when behavior changes
  • Remove abstractions that create more exceptions than consistency