Tailwind CSS v4 Setup & Workflow
Status: ✅ Up to date for Tailwind CSS v4.2.x
Table of Contents
- Overview
- Architecture
- Package Dependencies
- globals.css Structure
- Design Token Flow
- Dark Mode
- MFE Integration
- Component Usage
- Troubleshooting
Overview
The Kulcare platform uses Tailwind CSS v4 with a CSS-first configuration approach. All styling is centralized in the @kulcare/ui-kit package, which is shared across all micro-frontend apps (MFEs).
Key Technologies
- Tailwind CSS v4.2.x: Utility-first CSS framework with CSS-first configuration
- CSS Custom Properties: Runtime theme switching with HSL color format
@tailwindcss/vite: Official Vite plugin for Tailwind v4@theme inline: CSS-first theme configuration in Tailwind v4- tw-animate-css: Animation library (shadcn v4 compatible)
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND MONOREPO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ ui-kit │ ← Theme provider (globals.css) │
│ │ (shared) │ • Design tokens (:root, .dark) │
│ │ │ • @theme inline (Tailwind mappings) │
│ └──────┬──────┘ • Components (button, card, dialog, etc.) │
│ │ │
│ │ @import │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ MFEs (Micro-Frontends) │ │
│ ├──────────┬──────────┬──────────┬──────────┬─────────────┤ │
│ │ shell │ cms │ growth │ reports │ connect │ │
│ │ :3000 │ :3001 │ :3003 │ :3004 │ :3005 │ │
│ └──────────┴──────────┴──────────┴──────────┴─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘Key Principles
- Single Source of Truth: One
globals.cssin ui-kit = consistent theme across all apps - No CSS Duplication: Each app imports, Vite deduplicates during build
- Independent Builds: Each MFE can build independently or as part of shell
- CSS-First Config: Theme configuration uses
@theme inlinein CSS, nottailwind.config.js
Package Dependencies
ui-kit/package.json
{
"devDependencies": {
"@tailwindcss/vite": "^4.0.0", // Vite plugin for Tailwind v4
"tailwindcss": "4.2.2", // Tailwind CSS v4
"tw-animate-css": "^1.0.0" // Animation library (shadcn v4 compatible)
}
}MFE package.json (e.g., shell, cms, growth, reports, connect)
{
"devDependencies": {
"@tailwindcss/vite": "^4.2.2", // Vite plugin
"tailwindcss": "^4.2.2" // Tailwind CSS v4
}
}Removed Dependencies (No Longer Needed in v4)
"autoprefixer": "^10.4.x" // ❌ Removed - handled by @tailwindcss/vite
"postcss": "^8.4.x" // ❌ Removed - not used in v4
"tailwindcss": "^3.4.x" // ❌ Removed - upgraded to v4Vite Configuration
All MFEs use the same pattern
// apps/shell/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(), // ← Tailwind v4 plugin
],
})Why This Works
@tailwindcss/vitereplaces the old PostCSS pipeline- It processes CSS at build time, providing 3-5x faster builds
- No
postcss.config.jsortailwind.config.jsneeded
globals.css Structure
/* ═══════════════════════════════════════════════════════════════
1. FONTS & EXTERNAL IMPORTS
═══════════════════════════════════════════════════════════════ */
@import url('https://fonts.googleapis.com/css2?family=Mulish...');
@import "react-day-picker/style.css";
/* ═══════════════════════════════════════════════════════════════
2. TAILWIND v4 CORE
═══════════════════════════════════════════════════════════════ */
@import "tailwindcss"; // Tailwind's base styles
@import "tw-animate-css"; // Animation utilities
/* ═══════════════════════════════════════════════════════════════
3. DESIGN TOKENS (:root - Light Mode)
═══════════════════════════════════════════════════════════════ */
:root {
/* Core colors - Legacy app design system */
--background: hsl(0 0% 96%);
--foreground: hsl(228 14% 24%);
/* Component colors */
--card: hsl(0 0% 100%);
--card-foreground: hsl(228 14% 24%);
--primary: hsl(211 100% 52%);
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(0 0% 95%);
--secondary-foreground: hsl(228 14% 24%);
--muted: hsl(0 0% 95%);
--muted-foreground: hsl(0 0% 44%);
--accent: hsl(210 18% 92%);
--accent-foreground: hsl(228 14% 24%);
--destructive: hsl(0 84% 60%);
--destructive-foreground: hsl(0 0% 100%);
/* Utility colors */
--border: hsl(210 18% 92%);
--input: hsl(0 0% 85%);
--ring: hsl(211 100% 52%);
/* Radius - Legacy 3px base */
--radius: 0.1875rem;
/* Semantic - Legacy status colors */
--success: hsl(142 71% 45%); /* Green - universally understood as success */
--warning: hsl(38 92% 50%); /* Amber-orange - clear caution, high contrast */
--info: hsl(199 89% 48%); /* Sky blue - informational */
/* Fonts */
--font-sans: 'Mulish', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Shadows - Light mode (subtle black shadows) */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.10);
--shadow-lg: 0 7px 14px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.06);
--shadow-xl: 0 5px 25px rgba(0, 0, 0, 0.15);
/* Sidebar - Light mode (adaptive) */
--sidebar: hsl(220 10% 94%);
--sidebar-foreground: hsl(228 14% 24%);
--sidebar-primary: hsl(211 100% 52%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(211 100% 52% / 0.1);
--sidebar-accent-foreground: hsl(211 100% 52%);
--sidebar-border: hsl(210 18% 92%);
--sidebar-ring: hsl(211 100% 52%);
/* Sidebar Dimensions */
--sidebar-width: 16rem;
--sidebar-width-icon: 3rem;
--sidebar-width-mobile: 18rem;
/* Accent Palette (raw HSL channels for hsl() wrapping in @theme) */
/* Hues spaced ≥15° apart from each other and from primary (211°) */
--accent-indigo: 243 75% 59%;
--accent-indigo-subtle: 243 75% 96%;
--accent-purple: 262 83% 58%;
--accent-purple-subtle: 262 83% 96%;
--accent-pink: 336 80% 58%;
--accent-pink-subtle: 336 80% 96%;
--accent-cyan: 188 95% 42%;
--accent-cyan-subtle: 188 95% 94%;
--accent-teal: 172 66% 40%;
--accent-teal-subtle: 172 66% 94%;
--accent-orange: 24 94% 50%;
--accent-orange-subtle: 24 94% 94%;
/* Gray Scale (raw HSL channels) */
--gray-50: 210 20% 98%;
--gray-100: 210 20% 96%;
--gray-200: 214 20% 91%;
--gray-300: 213 20% 86%;
--gray-400: 215 16% 77%;
--gray-500: 215 16% 47%;
--gray-600: 215 19% 35%;
--gray-700: 215 25% 27%;
--gray-800: 217 33% 17%;
--gray-900: 222 47% 11%;
--gray-foreground: 0 0% 98%;
}
/* ═══════════════════════════════════════════════════════════════
4. DESIGN TOKENS (.dark - Dark Mode)
═══════════════════════════════════════════════════════════════ */
.dark {
--background: hsl(220 10% 10%);
--foreground: hsl(0 0% 98%);
--primary: hsl(211 100% 52%);
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(220 10% 15%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(220 10% 15%);
--muted-foreground: hsl(0 0% 65%);
--accent: hsl(220 10% 20%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 84% 55%);
--destructive-foreground: hsl(0 0% 100%);
--border: hsl(220 10% 20%);
--input: hsl(220 10% 18%);
/* Shadows - Dark mode (higher opacity for visibility) */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.15);
--shadow: 0 2px 8px rgba(0, 0, 0, 0.20);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.25);
--shadow-lg: 0 7px 14px rgba(0, 0, 0, 0.30), 0 3px 6px rgba(0, 0, 0, 0.15);
--shadow-xl: 0 5px 25px rgba(0, 0, 0, 0.15);
/* Sidebar - Dark mode (Ant Design dark sider #001529) */
--sidebar: hsl(220 27% 8%);
--sidebar-foreground: hsl(0 0% 65%);
--sidebar-primary: hsl(211 100% 52%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(220 13% 14%);
--sidebar-accent-foreground: hsl(211 100% 52%);
--sidebar-border: hsl(220 27% 12%);
--sidebar-ring: hsl(211 100% 52%);
/* Semantic Status (dark) - desaturated + darkened for dark bg contrast */
--success: hsl(142 60% 38%);
--warning: hsl(38 88% 45%);
--info: hsl(199 89% 48%);
/* Accent Palette — Dark Overrides */
/* Lighter hues for legibility on dark surfaces; subtle variants use 14-16% lightness */
--accent-indigo: 243 70% 68%;
--accent-indigo-subtle: 243 70% 16%;
--accent-purple: 262 75% 65%;
--accent-purple-subtle: 262 75% 16%;
--accent-pink: 336 72% 62%;
--accent-pink-subtle: 336 72% 16%;
--accent-cyan: 188 88% 50%;
--accent-cyan-subtle: 188 88% 14%;
--accent-teal: 172 60% 45%;
--accent-teal-subtle: 172 60% 14%;
--accent-orange: 24 90% 54%;
--accent-orange-subtle: 24 90% 16%;
/* Gray Scale — Dark Overrides */
--gray-50: 220 10% 10%;
--gray-100: 220 10% 15%;
--gray-200: 220 10% 20%;
--gray-300: 220 10% 25%;
--gray-400: 220 10% 35%;
--gray-500: 220 10% 50%;
--gray-600: 220 10% 65%;
--gray-700: 220 10% 75%;
--gray-800: 220 10% 85%;
--gray-900: 220 10% 92%;
--gray-foreground: 0 0% 98%;
}
/* ═══════════════════════════════════════════════════════════════
5. TAILWIND MAPPING (@theme inline)
═══════════════════════════════════════════════════════════════ */
@theme inline {
/* Core Colors */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
/* Semantic Status */
--color-success: var(--success);
--color-warning: var(--warning);
--color-info: var(--info);
/* Sidebar */
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* Sidebar Dimensions */
--sidebar-width: 16rem;
--sidebar-width-icon: 3rem;
--sidebar-width-mobile: 18rem;
/* Radius Scale */
--radius: var(--radius);
--radius-sm: calc(var(--radius) * 1.067);
--radius-md: var(--radius);
--radius-lg: calc(var(--radius) * 3.2);
--radius-xl: 0.5rem;
/* Fonts */
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
/* Shadows */
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: 0 5px 25px rgba(0, 0, 0, 0.15);
/* Chart Colors (recharts) */
--color-chart-1: var(--primary);
--color-chart-2: var(--secondary);
--color-chart-3: var(--accent);
--color-chart-4: var(--muted);
--color-chart-5: var(--destructive);
/* Typography Scale */
--text-xs: 0.75rem;
--text-xs--line-height: 1.5;
--text-sm: 0.75rem;
--text-sm--line-height: 1.5;
--text-base: 0.875rem;
--text-base--line-height: 1.5;
--text-md: 1rem;
--text-md--line-height: 1.5;
--text-lg: 1.125rem;
--text-lg--line-height: 1.4;
--text-xl: 1.25rem;
--text-xl--line-height: 1.35;
--text-2xl: 1.375rem;
--text-2xl--line-height: 1.35;
--text-3xl: 1.5rem;
--text-3xl--line-height: 1.3;
/* Leading Tokens */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* Accent Palette */
--color-accentIndigo: hsl(var(--accent-indigo));
--color-accentIndigo-subtle: hsl(var(--accent-indigo-subtle));
--color-accent-purple: hsl(var(--accent-purple));
--color-accent-purple-subtle: hsl(var(--accent-purple-subtle));
--color-accentPink: hsl(var(--accent-pink));
--color-accentPink-subtle: hsl(var(--accent-pink-subtle));
--color-accentCyan: hsl(var(--accent-cyan));
--color-accentCyan-subtle: hsl(var(--accent-cyan-subtle));
--color-accentTeal: hsl(var(--accent-teal));
--color-accentTeal-subtle: hsl(var(--accent-teal-subtle));
--color-accentOrange: hsl(var(--accent-orange));
--color-accentOrange-subtle: hsl(var(--accent-orange-subtle));
/* Gray Scale */
--color-gray-50: hsl(var(--gray-50));
--color-gray-100: hsl(var(--gray-100));
--color-gray-200: hsl(var(--gray-200));
--color-gray-300: hsl(var(--gray-300));
--color-gray-400: hsl(var(--gray-400));
--color-gray-500: hsl(var(--gray-500));
--color-gray-600: hsl(var(--gray-600));
--color-gray-700: hsl(var(--gray-700));
--color-gray-800: hsl(var(--gray-800));
--color-gray-900: hsl(var(--gray-900));
/* Kulcare Brand Colors */
--color-kulcare-primary: #078DFF;
--color-kulcare-primary-light: #3DA4FF;
--color-kulcare-primary-dark: #0570CC;
--color-kulcare-secondary: #722ed1;
/* App Identity Colors (do not theme-switch) */
--color-cms: #078DFF;
--color-cms-light: #e6f4ff;
--color-cms-dark: #0570CC;
--color-cms-accent: #3DA4FF;
--color-growth: #52c41a;
--color-growth-light: #f6ffed;
--color-growth-dark: #389e0d;
--color-growth-accent: #73d13d;
--color-doctor: #722ed1;
--color-doctor-light: #f9f0ff;
--color-doctor-dark: #531dab;
--color-doctor-accent: #9254de;
--color-connect: #1890ff;
--color-connect-light: #e6f7ff;
--color-connect-dark: #096dd9;
--color-connect-accent: #40a9ff;
--color-network: #fa8c16;
--color-network-light: #fff7e6;
--color-network-dark: #d46b08;
--color-network-accent: #ffa940;
--color-hospital: #eb2f96;
--color-hospital-light: #fff0f6;
--color-hospital-dark: #c41d7f;
--color-hospital-accent: #f759ab;
--color-reports: #13c2c2;
--color-reports-light: #e6fffb;
--color-reports-dark: #08979c;
--color-reports-accent: #36cfc9;
}
/* ═══════════════════════════════════════════════════════════════
6. BASE STYLES
═══════════════════════════════════════════════════════════════ */
/* Override Tailwind v4's default border-color (currentColor) */
* {
border-color: var(--border);
}
html {
font-family: var(--font-sans);
}
body {
background: var(--background);
color: var(--foreground);
}
/* ═══════════════════════════════════════════════════════════════
7. CUSTOM UTILITIES (@utility)
═══════════════════════════════════════════════════════════════ */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}File Location
packages/ui-kit/src/styles/globals.cssDesign Token Flow
┌─────────────────────────────────────────────────────────────────┐
│ DESIGN TOKEN FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. CSS Variables (globals.css) │
│ :root { --primary: hsl(211 100% 52%); } │
│ .dark { --primary: hsl(211 90% 56%); } │
│ │ │
│ ▼ │
│ 2. @theme inline mapping │
│ @theme inline { │
│ --color-primary: var(--primary); // Maps to bg-primary │
│ } │
│ │ │
│ ▼ │
│ 3. Tailwind Utilities (in components) │
│ <div className="bg-primary text-primary-foreground"> │
│ │
└─────────────────────────────────────────────────────────────────┘Token Naming Convention
| CSS Variable | Tailwind Utility | Usage |
|---|---|---|
--background | bg-background, text-background | Page background |
--foreground | text-foreground | Primary text |
--primary | bg-primary, text-primary | Brand color |
--primary-foreground | text-primary-foreground | Text on primary |
--secondary | bg-secondary | Secondary elements |
--muted | bg-muted, text-muted-foreground | Subtle backgrounds |
--destructive | bg-destructive | Error/danger states |
--border | border, border-border | Borders |
--radius | rounded, rounded-sm, etc. | Border radius |
--sidebar | bg-sidebar, text-sidebar-foreground | Sidebar (adaptive) |
--sidebar-primary | bg-sidebar-primary, text-sidebar-primary-foreground | Sidebar active item |
Dark Mode
How Dark Mode Works
-
User toggles dark mode → Adds/removes
.darkclass on<html>element -
CSS variable cascade:
/* Without .dark class - uses :root values */ --primary: hsl(211 100% 52%); // Light blue /* With .dark class - .dark values override */ .dark { --primary: hsl(211 90% 56%); // Brighter blue } -
@theme inlinepropagates to Tailwind:.dark { --primary: hsl(211 90% 56%); } @theme inline { --color-primary: var(--primary); } /* → bg-primary now uses the dark mode value automatically */
Implementation
// ThemeProvider.tsx
import { useTheme } from '@/hooks/use-theme'
function App() {
const { theme, setTheme } = useTheme()
return (
<div className={theme}>
{/* App content */}
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
)
}MFE Integration
How Each MFE Gets Styles
Each MFE imports the shared globals.css:
/* apps/shell/src/index.css */
@import "@kulcare/ui-kit/src/styles/globals.css";
/* Animation for line charts */
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
.animate-draw {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 2s ease-out forwards;
}/* apps/cms/src/index.css */
@import "@kulcare/ui-kit/src/styles/globals.css";
/* CMS-specific video session styles... */MFE Package Configuration
| App | Port | Tailwind Version | Status |
|---|---|---|---|
| shell | 3000 | v4.2.2 | ✅ Production |
| cms | 3001 | v4.2.2 | ✅ Production |
| growth | 3003 | v4.2.2 | ✅ Production |
| reports | 3004 | v4.2.2 | ✅ Production |
| connect | 3005 | v4.2.2 | ✅ Production |
| ui-kit | - | v4.2.2 | ✅ Theme provider |
Component Usage
Using Theme Tokens in Components
// ✅ Good - Uses theme tokens
<div className="bg-primary text-primary-foreground">
<button className="bg-destructive hover:bg-destructive/90">
Delete
</button>
</div>
// ❌ Bad - Hardcoded colors
<div className="bg-blue-500 text-white">
<button className="bg-red-600 hover:bg-red-700">
Delete
</button>
</div>Using shadcn Components
import { Button } from "@kulcare/ui-kit"
import { Card, CardHeader, CardContent } from "@kulcare/ui-kit"
function MyPage() {
return (
<Card>
<CardHeader>
<CardTitle>Patient Details</CardTitle>
</CardHeader>
<CardContent>
<Button variant="default">Save</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
</CardContent>
</Card>
)
}Button Variants
| Variant | Usage |
|---|---|
default | Primary actions - bg-primary text-primary-foreground |
destructive | Danger actions - bg-destructive text-destructive-foreground |
outline | Secondary actions - border border-input bg-background |
secondary | Less prominent - bg-secondary text-secondary-foreground |
ghost | Subtle actions - hover:bg-accent hover:text-accent-foreground |
link | Text links - text-primary underline-offset-4 hover:underline |
Troubleshooting
LSP Shows "Tailwind-specific syntax is disabled"
This is an IDE warning only, not a build error. The LSP doesn't understand Tailwind v4's @theme inline syntax.
Solution: Install the Tailwind CSS IntelliSense extension for VS Code, or ignore the warning - builds work correctly.
Styles Not Applying in MFE
-
Verify the MFE imports
globals.css:/* apps/*/src/index.css */ @import "@kulcare/ui-kit/src/styles/globals.css"; -
Verify the MFE has
@tailwindcss/vitein itsvite.config.ts:import tailwindcss from '@tailwindcss/vite' plugins: [react(), tailwindcss()]
Dark Mode Not Working
- Verify
.darkclass is being applied to<html>or<body> - Check that CSS variables are defined in both
:rootand.dark - Verify
@theme inlinereferences usevar()not direct values
Build Errors After Upgrading
-
Clear the build cache:
pnpm clean pnpm install pnpm build -
Check for mixed Tailwind versions:
pnpm list tailwindcss
Migration Notes
Upgrading from v3 to v4
See Migration Plan for detailed instructions.
Key Changes from v3
| v3 (Old) | v4 (New) |
|---|---|
tailwind.config.js | @theme inline in CSS |
@tailwind base/components/utilities | @import "tailwindcss" |
tailwindcss-animate | tw-animate-css |
| PostCSS plugin | @tailwindcss/vite |
hsl(var(--x)) | var(--x) directly |
shadow-xs | shadow-sm (shadow-xs removed in v4) |