Typescript monorepo with Tailwind v4 and shadcn
When we start a new project, one of the important things we consider is the scalability of the codebase. A well-configured monorepo makes development clean, organized, and a lot easier and fun to work with.
In this post, we’ll look at how to structure a monorepo with Tailwind v4 + shadcn, and see what are the common challenges in creating it.
This article is not a step-by-step guide, I’ll focus on the common pitfalls while creating a monorepo with tailwind and shadcn. All the code for this article is available in Github.
.
├── app
│ └── web
├── biome.json
├── lefthook.yaml
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── shared
│ ├── ts-config
│ └── ui
└── turbo.json
Features
Our monorepo have the following features:
- Tailwind v4 + shadcn
- Turborepo + pnpm workspaces
- Biome for lint + formatting
- Lefthook for pre-commit checks
- TypeScript path aliases (internal packages work just like npm imports).
Problem with Tailwind Source
In a normal single-app setup, Tailwind just works. especially with Tailwind v4 + Vite it’s even easier — all your files live inside src/, Tailwind scans them and generates CSS without any extra setup.
react-app/src/**/*.tsx → tailwind scans → css generated
But in a monorepo, Now our UI is split into multiple packages, and Tailwind won’t know where to look unless we tell it. If it doesn’t see the components, it won’t generate styles for them.
Example structure:
.
├── app
│ └── web
├── shared
│ ├── editor
│ ├── editor_compiled
│ ├── ts-config
│ └── ui
└── turbo.json
So, if we keep components inside shared or packages, we must point Tailwind to those folders manually using the new @Source directive:
@import "@acme/ui/globals.css";
@source './../../../shared/**/src/**/*.{ts,tsx}';
@source './../../../packages/**/src/**/*.{ts,tsx}';
Problem with compiled packages
In TypeScript monorepos, there are two main ways to build packages:
- JIT packages → export
.tsfile, compile where used (Eg.app/web) - Compiled packages → build once, import the compiled
.jselsewhere
The second approach causes issues with Tailwind:
- Needs a separate build step (
tsc/tsup) - Each package must install its own dependencies(Eg. React).
- Tailwind won’t see classnames unless you run PostCSS/Tailwind cli.
- Dynamic classnames props(commonly used in shadcn) is impossible to compile.
Problem with TS path aliases
Path aliases make the developer experience smooth. But working with path aliases in a monorepo can get complicated. There are two things to consider about path aliases based on where you import components:
- Within the package - This follows the path aliases you defined in
tsconfig.json - With other packages - This follows the path in exports defined in
package.json
Here you can see that the import statement in app/web is based on exports, where folders like hook and lib are not in shared/ui.
If you use the same name for path aliases and package names, searching by package name becomes difficult since two sources use the same name.