Back to blog
Tutorial

Simple design system with Tailwind

By Flávio Emanuel · · 8 min read

I use Tailwind for everything. But early on, every project was a visual mess. Different colors, random spacing, components that didn’t match. Then I built a minimal design system that works across all my projects. Three lines of config, huge payoff.

What every solo dev needs is consistency. You don’t need Shadcn, Chakra, or Material. Just well-organized Tailwind, clear tokens, and reusable components.

Color tokens

Start with tailwind.config.js. Extend the default colors with your palette.

export default {
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#f0f9ff",
          100: "#e0f2fe",
          500: "#0ea5e9",
          600: "#0284c7",
          900: "#082f49"
        },
        surface: {
          50: "#fafafa",
          100: "#f5f5f5",
          500: "#737373",
          900: "#171717"
        }
      }
    }
  }
}

Name by function, not by color. Don’t use blue-500, use brand-500. Not gray-100, use surface-100. When you want to change the palette, you update here and everything follows.

Keep the palette small. Five tones per color. You don’t need twelve variations.

Typography

Define weight, size, and line-height once.

extend: {
  fontSize: {
    xs: ["0.75rem", { lineHeight: "1rem" }],
    sm: ["0.875rem", { lineHeight: "1.25rem" }],
    base: ["1rem", { lineHeight: "1.5rem" }],
    lg: ["1.125rem", { lineHeight: "1.75rem" }],
    xl: ["1.25rem", { lineHeight: "1.75rem" }],
    "2xl": ["1.5rem", { lineHeight: "2rem" }]
  },
  fontFamily: {
    sans: ["Inter", "sans-serif"]
  }
}

Use one font. Inter, Poppins, Nunito. One. Then you’re not switching fonts mid-project.

Weights: 400 normal, 500 semi-bold, 600 bold. Three total. Your designs stay cleaner.

Spacing

Tailwind already has a solid spacing system. Only extend if you need to.

extend: {
  spacing: {
    gutter: "2rem",
    section: "4rem"
  }
}

Use gap-4, p-3, m-2 normally. Tailwind’s defaults work.

Tip: use spacing variables for large sections. Put custom spacing on full-width components.

Base components

Put these in a separate file. Like components/base.css or directly in your components.

Basic button:

export function Button({ variant = "primary", children, ...props }) {
  const baseStyles = "px-4 py-2 rounded-lg font-medium transition-colors"
  const variants = {
    primary: "bg-brand-500 text-white hover:bg-brand-600",
    secondary: "bg-surface-100 text-surface-900 hover:bg-surface-200",
    ghost: "bg-transparent text-brand-500 hover:bg-brand-50"
  }
  return <button className={`${baseStyles} ${variants[variant]}`} {...props}>{children}</button>
}

Simple card:

export function Card({ children }) {
  return <div className="bg-white rounded-lg shadow-sm p-6 border border-surface-100">{children}</div>
}

Consistent input:

export function Input({ label, ...props }) {
  return (
    <div>
      {label && <label className="block text-sm font-medium mb-2 text-surface-900">{label}</label>}
      <input
        className="w-full px-3 py-2 border border-surface-200 rounded-lg focus:outline-none focus:border-brand-500"
        {...props}
      />
    </div>
  )
}

Done. You have a button that looks like a button. A card that looks like a card. Everything cohesive.

Reusing across projects

Put these components in a lib/components folder. When you start a new project, copy the whole folder.

Or version it in a private GitHub repo. Import directly.

Each new project takes 20 minutes to set up the palette, plus 30 minutes tweaking colors for the client. Finished.

Why not use UI frameworks

Shadcn is great. But it brings dependencies, build configuration, headless components you have to style anyway.

If you work with small clients who want “simple” sites, heavy libraries don’t pay off.

Pure Tailwind works. Stays fast, you control everything, components are yours.

Practical structure

When I start a new Astro project:

src/
  lib/
    colors.ts (export tokens)
    spacing.ts (export spacing)
  components/
    Button.astro
    Card.astro
    Input.astro
  pages/
    index.astro

File lib/colors.ts:

export const colors = {
  brand: "brand-500",
  brandHover: "brand-600",
  surface: "surface-100",
  text: "surface-900"
}

Components import from there. Global change, one line.

Keeping consistency

When you’re redesigning a client’s portfolio, reuse the same palette everywhere. Same spacing. Same text sizes.

Their site looks cohesive. Layout feels intentional. Not by accident, it’s the design system.

For me, this lifted the quality of my projects. Clients notice when everything fits together.

Read also: React vs Astro | Astro 5: what changed | Case study GPM2

  • Create color tokens named by function
  • Define typography with one font, three weights
  • Use default Tailwind spacing
  • Build working Button, Card, Input components
  • Export colors.ts and reuse across projects
  • Version components in lib folder
  • Test palette works in different contexts

Design systems aren’t complicated. It’s just making decisions clear.

Why design system for solo devs

Solo devs save time. Without a design system, every project is a reset. Different palette, different components, layout doesn’t match. Design system transfers 80% of styling decisions from the current project to the previous one.

Clients see improved quality. Sites have stronger visual identity because they’re using consistent palette. Layouts feel intentional, not accidental.

Maintenance gets easier. Client comes back six months later to add a feature. You already know the palette, spacing, don’t need to rediscover.

Portfolio looks more professional. When you show five projects, they all look like they’re from the same studio because they share visual language. Doesn’t look like each was done by a different person.

I talked to 12 devs in community groups. The ones with solid design systems delivered 25% faster. Without sacrificing quality.

Avoiding over-engineering

There’s temptation to make the design system too complex. “I’ll add five button variations.” Don’t. If you don’t use it, it’s weight.

My design system has three color tones per variable. One font. Three weights. Three base sizes. Everything else is combination. Simple, replicable, works in production.

I started trying to clone all of Tailwind. Generated 500 lines of config. Then I finally grabbed it and deleted everything I never used. Result? 50 lines. Four projects running. Nothing broke.

When to expand the system

As you take on more clients with similar patterns, your design system grows naturally. Don’t force it. Only add when you see you need it on 2+ projects.

Example: I took three clinic clients. All three wanted timeline for “our story”. Then I created a reusable Timeline component. Now it’s part of the system.

But one client asked for a testimonial carousel. Never used it again. Didn’t go into the system. System stays light.

Final structure

Version everything in a private repo. When you start a new project, pull the template.

design-system-v2/
  tailwind.config.js
  src/
    lib/
      colors.ts
      spacing.ts
      typography.ts
    components/
      Button.astro
      Card.astro
      Input.astro
      Badge.astro
      Modal.astro
    patterns/
      Header.astro
      Footer.astro
      Hero.astro

Repo comes with README explaining the system. Add screenshots of how components look. New project, clone the repo, customize colors for the client, start building.

Design system is code infrastructure, not a product. Don’t spend too much time here. Spend it keeping it practical.

Migrating between projects

The part that saves the most time is when I pick up a new dental clinic project. Clone the design system repo, open tailwind.config.js, swap 4 colors and done. The new project already has buttons, cards, inputs, header and footer working. What used to take 2 days of setup now takes 40 minutes.

With Family Pilates, the design system let me focus on content from day one. No time wasted deciding font sizes or section spacing. Everything was already defined. Result: delivered 5 days ahead of schedule.

When the client requests a revision, the change is surgical. “I want the blue darker.” Swap one CSS variable. All buttons, links and highlights change together. No find-and-replace across 30 different files.

Design system vs component library

People confuse the two. Component libraries are Shadcn, Radix, Material UI. They’re pre-built, opinionated, heavy. A design system is yours. It’s the set of visual decisions you apply across all projects.

Custom design systemPre-built library
ControlTotalLimited to what the lib offers
WeightMinimal (only what you use)Heavy (imports entire package)
Learning curveNone (you built it)Need to learn the lib’s API
ConsistencyGuaranteed across projectsDepends on lib version
MaintenanceYou maintainLib team maintains

For solo devs working with sites and LPs, a custom design system wins. For a 10-person team building complex SaaS, a pre-built library makes more sense. Know where you stand.

Read more about stack decisions in React vs Astro and about delivering faster in deliver projects faster with AI.

  • Color tokens named by function, not by color name
  • Typography with one font and three weights
  • Spacing using Tailwind’s default scale
  • Base components working (Button, Card, Input)
  • Config versioned in a private repo
  • README with component screenshots
  • Tested palette in light and dark mode

A good design system is one you actually use. If it sits in a repo and never gets cloned, it’s not a system. It’s decoration.

Next step

Need a dev who truly delivers?

Whether it's a one-time project, team reinforcement, or a long-term partnership. Let's talk.

Chat on WhatsApp

I reply within 2 hours during business hours.