Skip to content
@aploon

Building a Consistent Design System with Tailwind CSS and shadcn/ui

  • #tailwindcss
  • #shadcnui
  • #frontend
  • #designsystem

A consistent user interface is not created by choosing attractive colors or using the same button component everywhere. It requires a shared set of design decisions, reusable components, predictable spacing, and clear conventions that developers can apply throughout an application.

Tailwind CSS and shadcn/ui provide a practical foundation for building such a system in React applications.

Tailwind CSS offers utility classes for styling, while shadcn/ui provides accessible, customizable component implementations that developers can adapt to their own design requirements.

The key is to treat these tools as parts of a coherent system rather than as independent styling solutions.

1. Establish design tokens first

Design tokens are the reusable values that define the visual identity of an application. They include colors, typography, spacing, borders, and shadows.

Instead of selecting slightly different colors for every component, define a limited palette and reuse it consistently.

For example, a Tailwind-based project might use semantic utility classes such as:

<div className="bg-background text-foreground">
  <h1 className="text-2xl font-semibold tracking-tight">
    Project dashboard
  </h1>
  <p className="text-muted-foreground">
    Manage your projects and activity.
  </p>
</div>

In a shadcn/ui project, semantic classes such as bg-background, text-foreground, and text-muted-foreground are typically connected to theme variables.

This approach separates the meaning of a color from its exact visual value. A muted text color can change across the application without requiring developers to update every component manually.

2. Reuse components instead of repeating styles

Repeated styling is one of the first signs that a design system needs structure.

Consider a button used in several parts of an application. Without a shared component, developers may independently choose padding, font weight, border radius, and hover behavior.

A shared button component establishes one consistent implementation.

shadcn/ui already provides customizable components such as buttons, inputs, dialogs, dropdown menus, and cards. Components are generally added to the application source code, allowing developers to modify them directly.

For example:

import { Button } from "@/components/ui/button";
 
export function ProjectActions() {
  return (
    <div className="flex gap-2">
      <Button>Save project</Button>
      <Button variant="outline">Cancel</Button>
    </div>
  );
}

The exact import path depends on the project's configuration. The important principle is that both actions use the same component implementation.

3. Use variants to control visual differences

Not every button should look identical. Primary actions, secondary actions, destructive actions, and subtle actions have different purposes.

However, each variation should be intentional and centrally defined.

shadcn/ui commonly uses class-variance-authority (CVA) to define component variants.

An illustrative implementation might look like this:

import { cva } from "class-variance-authority";
 
const actionStyles = cva(
  "inline-flex items-center justify-center rounded-md font-medium transition-colors",
  {
    variants: {
      intent: {
        primary: "bg-primary text-primary-foreground",
        secondary: "bg-secondary text-secondary-foreground",
        danger: "bg-destructive text-white",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-sm",
        lg: "h-12 px-6 text-base",
      },
    },
    defaultVariants: {
      intent: "primary",
      size: "md",
    },
  }
);

This example illustrates the pattern rather than a complete production-ready component.

Variants create a controlled vocabulary for visual differences. Instead of inventing a new combination of utility classes every time, developers choose from the options supported by the design system.

4. Organize components by responsibility

A growing application benefits from a predictable component structure.

One possible organization is:

src/
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ ui/
โ”‚   โ”‚   โ”œโ”€โ”€ button.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ input.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ dialog.tsx
โ”‚   โ”‚   โ””โ”€โ”€ card.tsx
โ”‚   โ”œโ”€โ”€ layout/
โ”‚   โ”‚   โ”œโ”€โ”€ page-header.tsx
โ”‚   โ”‚   โ””โ”€โ”€ sidebar.tsx
โ”‚   โ””โ”€โ”€ projects/
โ”‚       โ”œโ”€โ”€ project-card.tsx
โ”‚       โ””โ”€โ”€ project-form.tsx
โ”œโ”€โ”€ lib/
โ””โ”€โ”€ styles/

This is an illustrative structure, not a requirement imposed by Tailwind CSS or shadcn/ui.

The ui directory contains generic building blocks. Layout components define shared page structures, while feature components combine those building blocks around specific business needs.

This distinction prevents generic components from becoming tightly coupled to one feature.

5. Make consistency compatible with accessibility

A component that looks consistent but behaves differently for keyboard users is not a complete design-system component.

When customizing interactive components, preserve:

  • Keyboard navigation and visible focus states.
  • Appropriate labels for form controls.
  • Clear validation and error messages.
  • Sufficient color contrast.
  • Dialog focus management and predictable dismissal behavior.
  • Disabled and loading states where applicable.

shadcn/ui uses accessible component primitives, but accessibility still depends on how components are configured, composed, and used.

For example, a destructive action should not be communicated only through color. Its label and surrounding context should also make the consequence clear.

6. Avoid turning the design system into a constraint

A design system should improve consistency without making every page look identical.

The objective is to standardize decisions that should remain stable while allowing feature-specific composition.

Useful standards include:

  • A shared spacing scale.
  • Semantic color tokens.
  • Consistent typography levels.
  • Reusable form and feedback patterns.
  • Defined component variants.
  • Clear responsive behavior.

Avoid creating an entirely new abstraction for every combination of classes. A component should generally be extracted when it represents a repeated pattern, a meaningful interface concept, or a behavior that deserves consistent treatment.

7. Keep the system maintainable

A design system evolves with the application. Changes to a theme, button, or form field may affect many screens, so consistency must be supported by validation.

Consider documenting component usage, reviewing shared-component changes carefully, and testing important states across different screen sizes.

For larger applications, visual regression tests and component previews can help detect unintended changes. Automated accessibility checks can also catch common problems, although they do not replace manual testing.

Conclusion

Tailwind CSS makes styling explicit and flexible, while shadcn/ui provides a customizable starting point for accessible interface components.

Together, they can support a maintainable design system when teams define semantic tokens, centralize repeated patterns, use controlled variants, and preserve accessibility.

The goal is not to eliminate every individual styling decision. It is to make the important decisions once and reuse them consistently throughout the application.