Untitled

Theming Guide: shadcn + Tailwind CSS in a Turborepo Monorepo#

Overview#

This guide explains the theming approach for our monorepo, which uses shadcn/ui components with Tailwind CSS 4 in a Turborepo workspace. The philosophy is simple: consistent design tokens expressed through utilities.

Core Principles#

1. Design Tokens = Your Design System#

CSS variables define your design system. Change one variable, update the entire system.

2. Tailwind Utilities = How You Apply Them#

Utilities are composable and predictable. No fighting with class hierarchies.

3. Component Variants = Controlled Variations#

When you need structured options, use class-variance-authority (CVA) for component variants.


Project Structure#

text

All apps import the same globals.css, ensuring consistency across the entire monorepo.


Method 1: CSS Variables (Primary)#

This is the main way to customize themes broadly.

Location#

packages/ui/src/styles/globals.css

How It Works#

shadcn uses CSS variables for colors that map to Tailwind utility classes. Define your colors once, use them everywhere:

css
css

Usage in Components#

These variables are used via Tailwind utility classes:

tsx
tsx

Why HSL Format?#

Colors are defined in HSL (Hue, Saturation, Lightness) without the hsl() wrapper:

css
css

This allows Tailwind to add opacity modifiers:

tsx
tsx

Method 2: Tailwind Config for Global Patterns#

Since we're using Tailwind CSS 4, you can define design tokens in CSS using the @theme directive.

Typography & Spacing#

css
css

Base Styles#

Apply defaults to all elements:

css
css

Method 3: Component Layer Utilities#

For repeating patterns that aren't quite full components but more than single utilities.

When to Use#

Only create these when:

  • The pattern appears 5+ times
  • It's not worth making a full React component
  • It's a specific combination that has semantic meaning
css
css

Usage#

tsx
tsx

App-Specific Theming#

All apps share the same theme from packages/ui/src/styles/globals.css. This is the simplest and most consistent approach.

tsx
tsx

Option B: App-Specific Overrides#

If an app needs unique styling, import the global theme first, then add overrides:

tsx
tsx
css
css

Option C: Scoped Theme Classes#

For page-specific themes, use scoped classes:

css
css
tsx
tsx

Do's and Don'ts#

✅ Do#

  1. Use CSS variables for colors and theme tokens

    css
    css
  2. Use Tailwind utilities in components

    tsx
    tsx
  3. Create component variants with CVA

    tsx
    tsx
  4. Use semantic color names

    • primary, secondary, destructive
    • Not blue, red, green
  5. Embrace utility-first approach

    • Inline utilities in JSX are expected and encouraged
    • Composition over abstraction
  6. Test both light and dark modes

    • Always define both :root and .dark variables

❌ Don't#

  1. Don't create abstracted CSS classes for everything

    css
    css
  2. Don't fight the inline utility approach

    tsx
    tsx
  3. Don't use hsl() wrapper in CSS variables

    css
    css
  4. Don't create semantic class names

    css
    css
  5. Don't override Tailwind defaults globally without reason

    css
    css
  6. Don't use !important to solve specificity issues

    • Use cn() helper to properly merge classes
    • Trust Tailwind's specificity model

Working with Existing Components#

Adding Variants#

When you need multiple versions of a component, use CVA:

tsx
tsx

Usage#

tsx
tsx

Class Merging with cn()#

Always use the cn() helper to merge classes properly:

tsx
tsx

The cn() helper (from clsx + tailwind-merge):

  1. Conditionally applies classes
  2. Deduplicates conflicting utilities
  3. Preserves the last class when conflicts occur
tsx
tsx

Color Palette Reference#

Semantic Colors#

Variable Purpose Example Usage
background Page background bg-background
foreground Primary text text-foreground
card Card backgrounds bg-card
card-foreground Card text text-card-foreground
primary Primary actions bg-primary
primary-foreground Text on primary text-primary-foreground
secondary Secondary actions bg-secondary
muted Subtle backgrounds bg-muted
accent Highlighted elements bg-accent
destructive Dangerous actions bg-destructive
border Borders border-border
input Input borders border-input
ring Focus rings ring-ring

Opacity Modifiers#

All colors support opacity:

tsx
tsx

Dark Mode#

Dark mode is handled through the .dark class and next-themes:

tsx
tsx

Toggle Dark Mode#

tsx
tsx

Workflow Summary#

  1. Define design tokens in packages/ui/src/styles/globals.css

    • Colors, spacing, typography, border radius
  2. Use Tailwind utilities everywhere

    • Compose utilities in component JSX
    • Don't create custom CSS classes
  3. Create component variants when needed

    • Use CVA for structured variations
    • Keep variants in the component file
  4. Override per-app only when necessary

    • Import global theme first
    • Add app-specific overrides second
  5. Change one variable, update entire system

    • Modify --primary, all primary buttons update
    • Modify --radius, all rounded elements update

Examples#

Example 1: Changing Brand Color#

css
css

Every button, link, and element using bg-primary now uses purple.

Example 2: Custom Card Styling#

tsx
tsx

Example 3: Form Input with Validation States#

tsx
tsx

Resources#


Summary#

The shadcn philosophy: Consistent design tokens expressed through utilities.

  • ✅ CSS variables for colors and theme tokens
  • ✅ Tailwind utilities for styling
  • ✅ Component variants (CVA) for structured variations
  • ❌ No custom CSS classes
  • ❌ No fighting the utility-first approach

Change one CSS variable → entire system updates. Need tweaks? Compose utilities per component. Need structure? Create component variants.

90% of your styling work is using utilities that reference design tokens.

Viewing as guest