Introduction
This article provides a detailed guide on how to migrate Justd, which by default uses Tailwind CSS v3, to the updated Tailwind CSS v4, ensuring a smooth transition.
Upgrading
First, it's important to know that Tailwind CSS provides a codemod CLI tool, npx @tailwindcss/upgrade@next
, which can be used to upgrade your current project. However, if you encounter any issues while running it, don’t worry—I’m here to guide you step by step through the process.
Additionally, you can visit the Tailwind CSS v4 beta documentation to ensure you fully understand what changes are required for your specific framework. For this guide, I'll focus on examples using Vite and Next.js.
Vite
npm install tailwindcss@next @tailwindcss/vite@next
Then, in your vite.config.ts
file, you can append the required plugin as follows:
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
tailwindcss()
],
});
Next.js
For Next.js, you'll need to install the PostCSS plugin provided by Tailwind CSS by running the following command.
npm install tailwindcss@next @tailwindcss/postcss@next
Open your postcss.config.mjs
, and add that plugin into it like so.
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
After completing the previous steps, we can now move on to the setup.
Default Justd CSS
First, I'll assume you already have Justd installed, which uses Tailwind CSS v3 by default. Typically, your CSS will look something like this:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--light: 223.81 0% 98%;
--dark: 239.95 9% 6%;
--bg: 0 0% 100%;
--fg: 239.93 9% 4%;
--primary: 216.77 100% 50%;
--primary-fg: 0 0% 100%;
--secondary: 240 4% 96%;
--secondary-fg: 240.01 6% 10%;
--tertiary: 0 0% 100%;
--tertiary-fg: 240 4% 16%;
--overlay: 0 0% 100%;
--overlay-fg: 239.93 9% 4%;
--muted: 240 4% 96%;
--muted-fg: 240.01 4% 46%;
--accent: 216.77 100% 50%;
--accent-fg: 0 0% 100%;
--accent-subtle: 216.92 99% 97%;
--accent-subtle-fg: 216.74 100% 40%;
--success: 161.17 91% 31%;
--success-fg: 151.77 82% 96%;
--info: 205.77 100% 50%;
--info-fg: 0 0% 100%;
--danger: 0.01 72% 51%;
--danger-fg: 360 86% 97%;
--warning: 43.2 96% 56.99%;
--warning-fg: 20.91 91% 14.1%;
--border: 240 6% 90%;
--input: 240 6% 90%;
--ring: var(--primary);
--toggle: 240.01 5% 84%;
--radius: 0.5rem;
--primary-chart: 216.74 100% 45%;
--secondary-chart: 219.83 100% 77%;
--tertiary-chart: 216.01 92% 60%;
--highlight-chart: 210 98% 78%;
--accent-chart: 210 98% 78%;
}
.dark {
--bg: 0 0% 0%;
--fg: 223.81 0% 98%;
--primary: 216.04 98% 52%;
--primary-fg: 0 0% 100%;
--secondary: 239.99 6% 11%;
--secondary-fg: 223.81 0% 98%;
--tertiary: 240.02 10% 6%;
--tertiary-fg: 239.99 4% 96%;
--accent: 216.04 98% 52%;
--accent-fg: 0 0% 100%;
--accent-subtle: 215.99 94% 6%;
--accent-subtle-fg: 204.92 100% 77%;
--overlay: 240.03 6% 6%;
--overlay-fg: 223.81 0% 98%;
--muted: 239.95 3% 16%;
--muted-fg: 240.01 5% 65%;
--info: 205.77 100% 50%;
--info-fg: 0 0% 100%;
--success: 161.17 91% 31%;
--success-fg: 151.77 82% 96%;
--ring: var(--primary);
--toggle: 239.99 5% 26%;
--border: 240.01 7.1% 15%;
--input: 239.95 3% 16%;
--primary-chart: 221.19 83% 53%;
--secondary-chart: 211.99 95% 68%;
--tertiary-chart: 216.01 92% 60%;
--highlight-chart: 210 98% 78%;
--accent-chart: 212 96% 87%;
}
}
@layer base {
html {
@apply scroll-smooth;
}
* {
@apply border-border;
font-feature-settings: "cv11", "ss01";
font-variation-settings: "opsz" 850;
text-rendering: optimizeLegibility;
scrollbar-width: thin;
}
body {
@apply bg-bg text-fg;
}
/* dark mode */
.dark {
scrollbar-width: thin;
@media (prefers-color-scheme: dark) {
* {
scrollbar-width: thin;
}
}
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 8px;
height: 8px;
}
*::-webkit-scrollbar-track {
background: transparent;
border-radius: 5px;
}
*::-webkit-scrollbar-thumb {
@apply bg-muted;
border-radius: 14px;
border: 3px solid transparent;
}
}
Now, with the new Tailwind CSS version, these updates will apply.
@import 'tailwindcss';
@plugin 'tailwindcss-animate';
@variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-sans), sans-serif;
--font-mono: var(--font-mono), monospace;
--color-light: var(--light);
--color-dark: var(--dark);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-toggle: var(--toggle);
--color-bg: var(--bg);
--color-fg: var(--fg);
--color-primary: var(--primary);
--color-primary-fg: var(--primary-fg);
--color-secondary: var(--secondary);
--color-secondary-fg: var(--secondary-fg);
--color-tertiary: var(--tertiary);
--color-tertiary-fg: var(--tertiary-fg);
--color-accent: var(--accent);
--color-accent-fg: var(--accent-fg);
--color-accent-subtle: var(--accent-subtle);
--color-accent-subtle-fg: var(--accent-subtle-fg);
--color-success: var(--success);
--color-success-fg: var(--success-fg);
--color-info: var(--info);
--color-info-fg: var(--info-fg);
--color-danger: var(--danger);
--color-danger-fg: var(--danger-fg);
--color-warning: var(--warning);
--color-warning-fg: var(--warning-fg);
--color-muted: var(--muted);
--color-muted-fg: var(--muted-fg);
--color-overlay: var(--overlay);
--color-overlay-fg: var(--overlay-fg);
--radius-3xl: calc(var(--radius) + 7.5px);
--radius-2xl: calc(var(--radius) + 5px);
--radius-xl: calc(var(--radius) + 2.5px);
--radius-lg: calc(var(--radius));
--radius-md: calc(var(--radius) - 2.5px);
--radius-sm: calc(var(--radius) - 5px);
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border, currentColor);
}
}
@layer base {
:root {
--light: oklch(0.985 0 0);
--dark: oklch(0.141 0.005 285.823);
--bg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--fg: oklch(0.141 0.005 285.823);
--primary: oklch(0.546 0.245 262.881);
--primary-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--secondary: oklch(0.967 0.001 286.375);
--secondary-fg: oklch(0.141 0.005 285.823);
--tertiary: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--tertiary-fg: oklch(0.141 0.005 285.823);
--overlay: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--overlay-fg: oklch(0.141 0.005 285.823);
--muted: oklch(0.967 0.001 286.375);
--muted-fg: oklch(0.552 0.016 285.938);
--accent: oklch(0.546 0.245 262.881);
--accent-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--accent-subtle: oklch(97.05% 0.01418224665972208 254.6041641690868);
--accent-subtle-fg: 216.74 100% 40%;
--success: oklch(0.596 0.145 163.225);
--success-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--info: oklch(0.588 0.158 241.966);
--info-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--danger: oklch(0.577 0.245 27.325);
--danger-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--warning: oklch(0.681 0.162 75.834);
--warning-fg: oklch(0.987 0.026 102.212);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.546 0.245 262.881);
--toggle: oklch(0.967 0.001 286.375);
--radius: 0.5rem;
--primary-chart: oklch(0.546 0.245 262.881);
--secondary-chart: oklch(0.809 0.105 251.813);
--tertiary-chart: oklch(0.623 0.214 259.815);
--highlight-chart: oklch(0.882 0.059 254.128);
--accent-chart: oklch(0.901 0.058 230.902);
}
.dark {
--bg: oklch(0% 0 0);
--fg: oklch(0.985 0 0);
--primary: oklch(0.546 0.245 262.881);
--primary-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--secondary: oklch(0.21 0.006 285.885);
--secondary-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--tertiary: oklch(0.21 0.006 285.885);
--tertiary-fg: 239.99 4% 96%;
--accent: oklch(0.985 0 0);
--accent-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--accent-subtle: oklch(23.19% 0.0222 269.09);
--accent-subtle-fg: oklch(0.707 0.165 254.624);
--overlay: oklch(0.21 0.006 285.885);
--overlay-fg: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-fg: oklch(0.705 0.015 286.067);
--info: oklch(0.588 0.158 241.966);
--info-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--success: oklch(0.596 0.145 163.225);
--success-fg: oklch(100% 3.5594404384177905e-8 106.37411429114086);
--ring: oklch(0.546 0.245 262.881);
--toggle: oklch(0.274 0.006 286.033);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--primary-chart: oklch(0.546 0.245 262.881);
--secondary-chart: oklch(0.809 0.105 251.813);
--tertiary-chart: oklch(0.707 0.165 254.624);
--highlight-chart: oklch(0.809 0.105 251.813);
--accent-chart: oklch(0.882 0.059 254.128);
}
}
@layer base {
html {
@apply scroll-smooth;
}
* {
@apply border-border;
font-feature-settings: "cv11", "ss01";
font-variation-settings: "opsz" 850;
text-rendering: optimizeLegibility;
scrollbar-width: thin;
}
body {
@apply bg-bg text-fg;
}
/* dark mode */
.dark {
scrollbar-width: thin;
@media (prefers-color-scheme: dark) {
* {
scrollbar-width: thin;
}
}
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 8px;
height: 8px;
}
*::-webkit-scrollbar-track {
background: transparent;
border-radius: 5px;
}
*::-webkit-scrollbar-thumb {
@apply bg-muted;
border-radius: 14px;
border: 3px solid transparent;
}
}
Yes, you're right! there's quite a bit to adjust, but that’s just how it works.
Tailwind RAC (React Aria Component)
You’ve probably wondered about tailwindcss-react-aria-component
(TRAC). Unfortunately, at the time of writing, TRAC doesn’t yet support Tailwind CSS v4. However, I assume updates will be released soon. Meanwhile, here’s what you can do to adapt:
- Utilities like
exiting
have changed todata-exiting
. - For variables, syntax such as
bg-[--button-border]
is now updated tobg-(--button-border)
. - If you're using popovers or similar components, changes like
placement-left
will now becomedata-[placement=left]
.
These are some of the new features and syntax updates you'll need to implement for compatibility.
Tailwind Config
Finally, you can remove your tailwind.config.ts
file, because everything is now on your css file.