Tailwind v4 + Base UI — Project Style Guide
Project: astro-react-proto (Flower Industry Directory) Stack: Astro + React islands, Tailwind v4, Base UI, CVA, tailwind-merge Theme: Light mode only, flower-themed OKLCH tokens
Table of Contents
- •Architecture Overview
- •Design Tokens
- •Component Pattern
- •Monorepo Style Sharing
- •Critical Rules
- •Common Issues & Fixes
- •Adding New Tokens
- •Reference Documentation
Architecture Overview
This project uses Tailwind v4's @theme block (not @theme inline) to define all design tokens directly in CSS. No tailwind.config.ts file — everything is in CSS.
Key differences from shadcn/ui pattern:
- •OKLCH color space (not HSL) — perceptually uniform, better for design
- •
@themeblock (not:rootvariables +@theme inlinemapping) - •Base UI (not Radix UI / shadcn) — headless primitives, zero styling opinions
- •No dark mode — light mode only with flower theme
- •No
components.json— components live inpackages/ui/
File Locations
packages/styles/src/base.css ← All design tokens (@theme block) packages/ui/src/ ← React components (Base UI + CVA) apps/web/astro.config.mjs ← Astro + @tailwindcss/vite integration
Design Tokens
All tokens are defined in packages/styles/src/base.css using Tailwind v4's @theme block:
@import "tailwindcss";
@theme {
/* Font families */
--font-heading: "Playfair Display", ui-serif, Georgia, ...;
--font-body: "Inter", ui-sans-serif, system-ui, ...;
/* Primary: Rose tones */
--color-primary: oklch(0.65 0.18 12);
--color-primary-hover: oklch(0.58 0.2 12);
--color-primary-foreground: oklch(0.98 0.01 12);
/* Secondary: Sage green */
--color-secondary: oklch(0.55 0.08 140);
/* Accent: Terracotta */
--color-accent: oklch(0.6 0.15 45);
/* Surfaces */
--color-background: oklch(0.99 0.005 90);
--color-surface: oklch(0.98 0.01 90);
--color-muted: oklch(0.94 0.015 90);
--color-border: oklch(0.88 0.02 90);
/* Text */
--color-foreground: oklch(0.2 0.02 90);
--color-foreground-muted: oklch(0.45 0.02 90);
/* Semantic */
--color-destructive: oklch(0.55 0.22 25);
--color-success: oklch(0.55 0.15 145);
/* Spacing, radius, shadows... */
--spacing-content: 1.5rem;
--radius-card: 1rem;
--shadow-card: 0 2px 8px oklch(0.2 0.02 90 / 0.08);
}
Usage in components:
<div className="bg-primary text-primary-foreground">Rose button</div> <div className="bg-surface shadow-card rounded-card p-content">Card</div> <p className="font-heading text-foreground">Heading</p> <p className="font-body text-foreground-muted">Body text</p>
Component Pattern: Base UI + CVA + Tailwind
Components use Base UI for accessible headless primitives, CVA for variant logic, and tailwind-merge for class composition.
Example Component
import { cva, type VariantProps } from "class-variance-authority";
import { twMerge } from "tailwind-merge";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-body text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary-hover",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-hover",
outline: "border border-border bg-transparent hover:bg-surface",
ghost: "hover:bg-surface",
destructive: "bg-destructive text-primary-foreground",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4",
lg: "h-12 px-6 text-base",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
function Button({ className, variant, size, ...props }: ButtonProps) {
return <button className={twMerge(buttonVariants({ variant, size }), className)} {...props} />;
}
Using Base UI Primitives
For complex interactive components (selects, dialogs, sliders), wrap Base UI:
import { Select } from "@base-ui/react";
function CategorySelect({ value, onChange, options }) {
return (
<Select.Root value={value} onValueChange={onChange}>
<Select.Trigger className="flex h-10 w-full items-center justify-between rounded-md border border-border bg-surface px-3 text-sm">
<Select.Value placeholder="Select category..." />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup className="rounded-md border border-border bg-surface p-1 shadow-elevated">
{options.map((opt) => (
<Select.Option
key={opt}
value={opt}
className="cursor-pointer rounded-sm px-2 py-1.5 text-sm hover:bg-muted"
>
<Select.OptionText>{opt}</Select.OptionText>
</Select.Option>
))}
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
}
Monorepo Style Sharing
Styles flow from the shared package to the app:
packages/styles/src/base.css → Defines @theme tokens packages/ui/ → Uses tokens in components apps/web/ → Imports styles package, renders components
In apps/web, Tailwind picks up the theme via the Vite plugin (@tailwindcss/vite).
Critical Rules
Always Do:
- •
Use
@themeblock for all design tokens (not:rootCSS variables)css@theme { --color-primary: oklch(0.65 0.18 12); /* ✅ */ } - •
Use OKLCH color space for all colors
css--color-accent: oklch(0.6 0.15 45); /* ✅ */ --color-accent: hsl(25 70% 50%); /* ❌ not this project */
- •
Use
twMerge()for class composition (not raw concatenation)tsxclassName={twMerge(baseClasses, className)} /* ✅ */ className={`${baseClasses} ${className}`} /* ❌ */ - •
Use CVA for component variants
- •
Use semantic token names in components
tsx<div className="bg-primary"> /* ✅ */ <div className="bg-rose-500"> /* ❌ use tokens */
Never Do:
- •
Don't add dark mode — this project is light mode only
- •
Don't use
tailwind.config.ts— v4 uses CSS@themeblock - •
Don't use shadcn/ui or Radix UI — this project uses Base UI
- •
Don't use HSL colors — project uses OKLCH
- •
Don't use
@apply(deprecated in Tailwind v4) - •
Don't use
:rootvariables for theme colors — use@themeblock directly
Common Issues & Fixes
| Symptom | Cause | Fix |
|---|---|---|
bg-primary doesn't work | Token not in @theme | Add to packages/styles/src/base.css |
| Color looks wrong | Mixing HSL/OKLCH | Use OKLCH values only |
| Utility not generating | Wrong token prefix | Use --color-* for colors, --spacing-* for spacing |
| Styles not applying in app | Missing @tailwindcss/vite | Check apps/web/astro.config.mjs has the Vite plugin |
| Component styles don't match | Raw string concat | Use twMerge() for class composition |
Adding New Tokens
To add a new design token:
- •Add to
packages/styles/src/base.cssinside the@themeblock - •Use the correct prefix for automatic utility generation:
@theme {
/* Colors → bg-*, text-*, border-* utilities */
--color-info: oklch(0.6 0.15 250);
/* Spacing → p-*, m-*, gap-* utilities */
--spacing-dialog: 2rem;
/* Radius → rounded-* utilities */
--radius-pill: 9999px;
/* Shadows → shadow-* utilities */
--shadow-focus: 0 0 0 3px oklch(0.65 0.18 12 / 0.3);
/* Fonts → font-* utilities */
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
Tailwind v4 Plugins
Tailwind v4 uses the @plugin directive in CSS (not require() in config):
@import "tailwindcss"; @plugin "@tailwindcss/typography"; @plugin "@tailwindcss/forms";
Common error:
- •❌
@import "@tailwindcss/typography"(doesn't work) - •✅
@plugin "@tailwindcss/typography"(correct)
Built-in features: Container queries are core in v4 (no plugin needed).
Reference Documentation
For deeper troubleshooting, the references/ directory contains:
- •common-gotchas.md — General Tailwind v4 pitfalls and fixes
- •dark-mode.md — Dark mode patterns (not used in this project, reference only)
- •migration-guide.md — Migrating from Tailwind v3 to v4
- •plugins-reference.md — Official Tailwind v4 plugins
- •advanced-usage.md — Advanced patterns and custom colors
Load these when encountering specific issues not covered above.