Fix Tailwind v4 scaffolding FOUC issues
WHY are these changes introduced?
Fixes critical scaffolding failures and test instability blocking first-party Tailwind CSS v4 support in Hydrogen. Enables Tailwind v4 as the default CSS framework for new projects with production-ready optimizations and comprehensive CLI integration.
WHAT is this pull request doing?
π― First-Party Tailwind v4 Integration
Before: After:
βββββββββββββββββββββββ βββββββββββββββββββββββ
β h2 init --quickstartβ β h2 init --quickstartβ
β ββ CSS: No default β β ββ CSS: Tailwind v4 β
βββββββββββββββββββββββ βββββββββββββββββββββββ
βββββββββββββββββββββββ βββββββββββββββββββββββ
β --styling tailwind β β --styling tailwind β
β ββ Remote templates β β ββ Full CLI Setup β
β had no CSS β β β
β strategy support β β β
βββββββββββββββββββββββ βββββββββββββββββββββββ
βββββββββββββββββββββββ βββββββββββββββββββββββ
β CLI Path Resolution β β CLI Path Resolution β
β ββ Hard-coded dist/ β β ββ Dynamic detectionβ
β paths only β β for both configs β
βββββββββββββββββββββββ βββββββββββββββββββββββ
| Component | Before | After | Impact |
|---|---|---|---|
| Quickstart | No default CSS | Tailwind v4 default | Zero-config styling |
| CLI Commands | --styling unsupported for remote |
Full CSS strategy | Reliable scaffolding |
| Version | v4.1.6 (beta) | v4.1.12 (stable) | Production ready |
| Build Output | Single path structure | Dynamic viteEnvironmentApi support | All CLI commands work |
π§ Core Infrastructure
CLI Path Resolution (Breaking Change)
React Router Build Outputs:
ββ viteEnvironmentApi: false (legacy)
β ββ dist/
β ββ index.js
β ββ metafile.server.json
ββ viteEnvironmentApi: true (modern)
ββ dist/
ββ client/
ββ server/
ββ index.js
ββ metafile.server.json
CLI Auto-Detection: All commands (build, deploy, preview, debug)
now dynamically resolve paths based on React Router config
CSS Strategy Integration
- Remote Templates: Add CSS strategy handling for
--styling tailwind - Import Resolution: Correct
appStylesβtailwindStylesreplacement - Performance: CSS preload optimization with
fetchPriority: 'high' - Bundle Strategy: Unified CSS (
cssCodeSplit: false) prevents route-based splitting
π¦ Files Changed
packages/cli/
βββ assets/tailwind/package.json β v4.1.12 stable
βββ src/commands/hydrogen/init.ts β Tailwind default for quickstart
βββ src/lib/onboarding/remote.ts β CSS strategy handling
βββ src/lib/setups/css/replacers.ts β Import replacement fixes
βββ src/lib/setups/css/tailwind.ts β Path corrections
βββ src/lib/vite-config.ts β viteEnvironmentApi support
βββ src/**/*.test.ts β Comprehensive test coverage
packages/hydrogen/
βββ src/vite/plugin.ts β cssCodeSplit: false
templates/skeleton/
βββ app/root.tsx β CSS preload optimization
βββ package.json β Exact dependency versions
βββ react-router.config.ts β viteEnvironmentApi: true
Test Coverage: 21 files changed, 2,122 insertions(+), 20 deletions(-)
Quality Assurance
E2E Test Pipeline
Scaffold β Verify Files β Install Deps β Type Check β Build β Validate
β β β β β β
Success All Present Clean Pass Success Optimized
- Complete Scaffolding Flow: From init β production build
- Cross-Language Support: TypeScript and JavaScript variants
- Version Matrix: viteEnvironmentApi enabled/disabled compatibility
- Regression Prevention: Import handling, dependency resolution
HOW to test your changes?
Prerequisites
# Ensure you're on the fix/tailwind-v4-fouc-clean branch
git checkout fix/tailwind-v4-fouc-clean
# Build the CLI
cd packages/cli
npm run build
Test 1: Quickstart Mode (Tailwind Default)
Setup
# Scaffold with quickstart (Tailwind is now default)
node dist/create-app.js --path /tmp/test-quickstart --quickstart --no-install-deps
Validation Commands
cd /tmp/test-quickstart
# 1. Verify Tailwind CSS file exists
ls -la app/styles/tailwind.css
# Expected: File exists with @import 'tailwindcss'
# 2. Check package.json has Tailwind v4.1.12
grep -E "tailwindcss|@tailwindcss/vite" package.json
# Expected:
# "tailwindcss": "^4.1.12"
# "@tailwindcss/vite": "^4.1.12",
# 3. Verify tailwindStyles import is present
grep "import.*Styles" app/root.jsx
# Expected:
# import resetStyles from '~/styles/reset.css?url';
# import tailwindStyles from '~/styles/tailwind.css?url';
# 4. Check fetchPriority optimization
grep "fetchPriority: 'high'" app/root.jsx
# Expected: 2 occurrences for CSS preload hints
# 5. Verify React Router viteEnvironmentApi flag
grep "unstable_viteEnvironmentApi: true" react-router.config.js
# Expected: Flag is present
# 6. Check Vite config has Tailwind plugin
grep "@tailwindcss/vite" vite.config.js
# Expected: import tailwindcss from '@tailwindcss/vite';
Test 2: Explicit Tailwind Setup (JavaScript)
Setup
# Use the local CLI from this branch
cd packages/cli
node dist/create-app.js --path /tmp/test-explicit-tailwind \
--template skeleton --styling tailwind --language js --no-install-deps
Validation Commands
cd /tmp/test-explicit-tailwind
# 1. JavaScript root file has correct imports
grep "import.*Styles" app/root.jsx
# Expected:
# import resetStyles from '~/styles/reset.css?url';
# import tailwindStyles from '~/styles/tailwind.css?url';
# 2. No appStyles references remain
grep "appStyles" app/root.jsx
# Expected: No matches (appStyles completely replaced)
# 3. Verify Tailwind CSS v4 syntax
cat app/styles/tailwind.css
# Expected: @import 'tailwindcss'; (NOT @tailwind directives)
# 4. Check package.json has Tailwind v4.1.12
grep -E "tailwindcss|@tailwindcss/vite" package.json
# Expected:
# "tailwindcss": "^4.1.12"
# "@tailwindcss/vite": "^4.1.12",
Test 3: Full Build Validation
Setup
cd /tmp/test-quickstart
npm install
npm run build
Validation
# 1. Build completes successfully
# Expected: dist/ directory created
# 2. CSS is not code-split
ls -la dist/client/*.css
# Expected: Single CSS file (not split per route)
# 3. Preview without styling flash
npm run preview
# Open http://localhost:3000
# Navigate between routes
# Expected: NO flash of unstyled content
Test 4: E2E Test Suite
Run Tests
cd packages/cli
npm run test:e2e
What the E2E Tests Validate
Test 1: Complete Tailwind v4 Project Scaffolding
- Step 1: Runs init command with Tailwind styling
- Step 2: Verifies all files created correctly
- Tailwind CSS file exists at
app/styles/tailwind.css - Package.json has
tailwindcss: ^4.1.12 - Package.json has
@tailwindcss/vite: ^4.1.12 - Root file imports
tailwindStyles(notappStyles) - CSS preload hints have
fetchPriority: 'high' - React Router config has
unstable_viteEnvironmentApi: true - Vite config includes Tailwind plugin
- No beta/alpha versions in dependencies
- Tailwind CSS file exists at
- Step 3: Installs dependencies
- Step 4: Runs TypeScript check (no errors)
- Step 5: Builds the project successfully
- Step 6: Verifies build output in
dist/ - Step 7: Validates CSS is not code-split (single CSS file)
Test 2: Quickstart Mode with Tailwind Default
- Verifies Tailwind is enabled by default in quickstart
- Checks all optimizations are applied
- Validates package.json has v4.1.12
Expected Console Output
Test project path: /tmp/...
Starting E2E test for Tailwind v4 scaffolding...
Step 1: Running init command...
Step 2: Verifying file creation...
Tailwind CSS file exists: true
Tailwind in dependencies: ^4.1.12
@tailwindcss/vite in devDependencies: ^4.1.12
Style imports found:
import resetStyles from '~/styles/reset.css?url';
import tailwindStyles from '~/styles/tailwind.css?url';
Step 3: Installing dependencies...
Step 4: Running TypeScript check...
Step 5: Building the project...
Step 6: Verifying build output...
Step 7: Validating optimizations...
Found 1-3 CSS files in build
E2E test completed successfully!
Testing quickstart mode with Tailwind...
Quickstart test completed successfully!
Note: Tests have 2-minute timeout to accommodate full build process. If npm install fails in CI due to React Router conflicts, the test may need retry.
Test 5: One-Line Validation Script
Run this comprehensive validation in one command:
# Clean test
rm -rf /tmp/tophat-test && \
cd packages/cli && \
node dist/create-app.js --path /tmp/tophat-test --quickstart --no-install-deps && \
cd /tmp/tophat-test && \
echo "=== VALIDATION RESULTS ===" && \
echo "1. Tailwind CSS:" && ls app/styles/tailwind.css 2>/dev/null && echo "File exists" || echo "Missing" && \
echo "2. Package.json:" && grep -q "tailwindcss.*4.1.12" package.json && echo "Has v4.1.12" || echo "Wrong version" && \
echo "3. Import:" && grep -q "import tailwindStyles" app/root.jsx && echo "Import present" || echo "Import missing" && \
echo "4. FetchPriority:" && grep -q "fetchPriority: 'high'" app/root.jsx && echo "Optimization applied" || echo "Missing" && \
echo "5. ViteEnvironmentApi:" && grep -q "viteEnvironmentApi: true" react-router.config.js && echo "Flag enabled" || echo "Missing" && \
echo "=== ALL CHECKS COMPLETE ==="
Expected Output:
=== VALIDATION RESULTS ===
1. Tailwind CSS:
app/styles/tailwind.css
File exists
2. Package.json:
Has v4.1.12
3. Import:
Import present
4. FetchPriority:
Optimization applied
5. ViteEnvironmentApi:
Flag enabled
=== ALL CHECKS COMPLETE ===
Key Fixes Implemented
1. Remote Template CSS Strategy Support
- Added
handleCssStrategytosetupRemoteTemplate - Fixed issue where
--styling tailwindhad no effect on remote templates
2. Import Statement Fix
- Fixed replacers to correctly replace
appStylesimport withtailwindStyles - Fixed import path generation to use
~/prefix
3. Performance Optimizations
fetchPriority: 'high'on CSS preload hintsunstable_viteEnvironmentApi: truein React Router configcssCodeSplit: falsein Hydrogen Vite plugin
4. Quickstart Default
- Tailwind CSS v4 now default for
--quickstartmode - Updated from v4.1.6 to v4.1.12 (latest stable)
Manual Fix (if needed)
If for any reason the import is missing after scaffolding, add this line after the resetStyles import in root.tsx/jsx:
import tailwindStyles from '~/styles/tailwind.css?url';
Success Criteria
All new Hydrogen projects with Tailwind CSS v4 should:
- Have no flash of unstyled content in production
- Use Tailwind CSS v4.1.12 (stable release)
- Have proper imports and no undefined variables
- Include all performance optimizations
- Work with both TypeScript and JavaScript
Impact: Tailwind v4 production-ready β’ Zero-config quickstart β’ All CLI commands compatible β’ Comprehensive test coverage