Tailwind CSS v4 + DaisyUI v5 Migration Guide
Tailwind CSS v4 + DaisyUI v5 Migration Guide
Guide for upgrading a Phoenix project from Tailwind CSS v3 + DaisyUI v4 to Tailwind CSS v4 + DaisyUI v5.
Overview of Changes
| Component | Before | After |
|---|---|---|
| Tailwind CSS | 3.x | 4.x |
| DaisyUI | 4.x | 5.x |
| Configuration |
tailwind.config.js |
CSS-native (app.css) |
Step 1: Update Versions
1.1 Update config/config.exs
# Change Tailwind version
config :tailwind,
version: "4.1.8", # Was: "3.4.3"
your_app: [
args: ~w(
--input=css/app.css
--output=../priv/static/assets/css/app.css
),
cd: Path.expand("../assets", __DIR__)
]
1.2 Update assets/package.json
{
"name": "your-app",
"version": "0.0.1",
"type": "module",
"devDependencies": {
"daisyui": "^5",
"tailwindcss": "^4"
}
}
Important: Add "type": "module" for ESM support.
1.3 Install Dependencies
cd assets && npm install
Step 2: CSS Configuration Migration
2.1 Delete assets/tailwind.config.js
This file is no longer needed - configuration moves to CSS.
2.2 Update assets/css/app.css
Before (Tailwind v3):
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
/* Custom styles */
After (Tailwind v4):
@import "tailwindcss";
/* DaisyUI plugin */
@plugin "daisyui";
/* Heroicons - IMPORTANT: import into base layer, not utilities! */
/* This ensures w-* h-* utilities override the default size */
@import "./heroicons.css" layer(base);
/* Content sources for Tailwind v4 */
@source "../js/**/*.js";
@source "../../lib/your_app_web.ex";
@source "../../lib/your_app_web/**/*.*ex";
/* PhoenixKit Integration (if used) */
@source "../../deps/phoenix_kit/lib/**/*.*ex";
@source "../../deps/phoenix_kit/lib/**/*.heex";
/* Custom theme extension */
@theme {
--color-brand: #FD4F00;
}
/* Fix select and option colors for dark theme */
@layer components {
select option {
@apply bg-base-100 text-base-content;
}
[data-theme="dark"] select {
color-scheme: dark;
}
[data-theme="light"] select {
color-scheme: light;
}
}
/* LiveView loading state variants */
@custom-variant phx-click-loading (&.phx-click-loading, .phx-click-loading &);
@custom-variant phx-submit-loading (&.phx-submit-loading, .phx-submit-loading &);
@custom-variant phx-change-loading (&.phx-change-loading, .phx-change-loading &);
Step 3: Heroicons for Tailwind v4
Tailwind v4 standalone CLI does not fully support JS plugins with matchComponents. The solution is to generate a static CSS file.
3.1 Create Generation Script
Create assets/generate-heroicons.mjs:
import fs from 'fs';
import path from 'path';
const iconsDir = './deps/heroicons/optimized';
let css = '/* Heroicons - auto-generated */\n';
const icons = [
['', '/24/outline'],
['-solid', '/24/solid'],
['-mini', '/20/solid'],
['-micro', '/16/solid'],
];
icons.forEach(([suffix, dir]) => {
const fullDir = path.join(iconsDir, dir);
fs.readdirSync(fullDir).forEach((file) => {
const name = path.basename(file, '.svg') + suffix;
const fullPath = path.join(fullDir, file);
const content = fs
.readFileSync(fullPath)
.toString()
.replace(/\r?\n|\r/g, '')
.replace(/"/g, "'")
.replace(/#/g, '%23');
css += `.hero-${name} {
-webkit-mask-image: url("data:image/svg+xml;utf8,${content}");
mask-image: url("data:image/svg+xml;utf8,${content}");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
background-color: currentColor;
vertical-align: middle;
display: inline-block;
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
}
`;
});
});
fs.writeFileSync('./assets/css/heroicons.css', css);
console.log('Generated heroicons.css with', (css.match(/\.hero-/g) || []).length, 'icons');
3.2 Generate CSS
node assets/generate-heroicons.mjs
3.3 Add to .gitignore (optional)
If you want to generate on each build:
assets/css/heroicons.css
Step 4: Update Vendor Files
4.1 Rename CommonJS Files
If you have vendor files in CommonJS format (e.g., topbar.js):
mv assets/vendor/topbar.js assets/vendor/topbar.cjs
4.2 Update Imports in app.js
// Before
import topbar from "../vendor/topbar";
// After
import topbar from "../vendor/topbar.cjs";
Step 5: DaisyUI v5 Changes
5.1 Changed Classes
| DaisyUI v4 | DaisyUI v5 |
|---|---|
select-bordered |
select |
placeholder (avatar) |
avatar-placeholder |
steps component |
Reworked, check documentation |
5.2 Update Components
Check your components for usage of changed classes:
grep -r "select-bordered" lib/
grep -r "placeholder" lib/ | grep -v "::placeholder"
Step 6: Build and Verify
6.1 Clear Cache
rm -rf _build/tailwind-*
rm -rf priv/static/assets
6.2 Build Assets
mix assets.deploy
6.3 Verify Result
# Should have many SVGs (for heroicons)
grep -c 'data:image/svg' priv/static/assets/css/app.css
# Check CSS size
ls -la priv/static/assets/css/
Troubleshooting
Problem: Icons display as black squares
Cause: CSS variables with SVG are not generated by the plugin.
Solution: Use a static CSS file with heroicons (see Step 3).
Problem: Cannot find module 'tailwindcss/plugin'
Cause: npm package tailwindcss is not installed.
Solution:
cd assets && npm install tailwindcss@4 --save-dev
Problem: MODULE_TYPELESS_PACKAGE_JSON warning
Cause: package.json does not specify module type.
Solution: Add "type": "module" to assets/package.json.
Problem: CommonJS files don’t work with ESM
Cause: After adding "type": "module", CommonJS files must have .cjs extension.
Solution: Rename .js to .cjs for CommonJS files.
Problem: Icons load huge, then shrink (FOUC)
Cause: Heroicons are imported into utilities layer, and their sizes conflict with w-* h-* utilities.
Solution:
-
Import heroicons.css into the
baselayer, notutilities:@import "./heroicons.css" layer(base); -
Perform a full cache clear and rebuild:
rm -rf _build/tailwind-* rm -rf priv/static/assets mix assets.deploy
Explanation: CSS Cascade Layers have priority order: base < components < utilities. When heroicons are in the base layer, their default sizes (1.5rem) are overridden by w-5 h-5 utilities from the utilities layer.