Rob Austin - Feb 18, 2025
We recently updated all of our shadcnblocks.com blocks to Tailwind 4. Here’s a detailed guide of the steps we took to make Tailwind 4 work seamlessly with shadcn/ui themeing.
For the official upgrade guide from Tailwind 3 => 4 visit the Tailwind docs Tailwind 4 Upgrade Guide.
At Shadcnblocks, we use Next.js, so we opted for the PostCSS installation (not the Vite version). Install the latest version of Tailwind via NPM.
npm install tailwindcss@next @tailwindcss/postcss@next
Next, update your PostCSS configuration to include the Tailwind plugin:
export default { plugins: { "@tailwindcss/postcss": {}, }}
Now let’s migrate our tailwind config.
The biggest change in Tailwind 4 is they have moved to to a CSS-first-configuration — Whcih means the tailwind.config.js
has been removed and all configuration now happens directly in the main CSS file.
In Next.js, the default stylesheet is located at src/app/globals.css
. So well be moving all our variables and values from the old tailwind config file into this CSS file. This shift simplifies the workflow by consolidating your configuration into a single CSS file, but it does require some initial adjustment.
@tailwind base;@tailwind components;@tailwind utilities;
@layer base { :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%; --card: 0 0% 100%; --card-foreground: 0 0% 3.9%; --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%; --primary: 0 0% 9%; --primary-foreground: 0 0% 98%; --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%; --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%; --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 3.9%; --radius: 0.5rem;
--chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%;
/* Shadcnblocks.com extra colors */ --muted-2: 0 0% 90%; --muted-2-foreground: 240 3.8% 46.1%; }
.dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; --card: 240 10% 3.9%; --card-foreground: 0 0% 98%; --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; --primary: 0 0% 98%; --primary-foreground: 240 5.9% 10%; --secondary: 240 3.7% 15.9%; --secondary-foreground: 0 0% 98%; --muted: 240 3.7% 15.9%; --muted-foreground: 240 5% 64.9%; --accent: 240 3.7% 15.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%;
/* Shadcnblocks.com extra colors */ --muted-2: 0, 0%, 18%; --muted-2-foreground: 240, 2%, 75%; }}
import type { Config } from "tailwindcss";
const config = { darkMode: ["class"], content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], theme: { container: { center: true, padding: "1rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, chart: { "1": "hsl(var(--chart-1))", "2": "hsl(var(--chart-2))", "3": "hsl(var(--chart-3))", "4": "hsl(var(--chart-4))", "5": "hsl(var(--chart-5))", }, muted-2: { DEFAULT: "hsl(var(--muted-2))", foreground: "hsl(var(--muted-2-foreground))", }, }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, "fade-in-out": { "0%": { opacity: "0", }, "20%": { opacity: "1", }, "80%": { opacity: "1", }, "100%": { opacity: "0", }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", "fade-in-out": "fade-in-out 4s ease-in-out", }, }, }, plugins: [ require("tailwindcss-animate"), require("@tailwindcss/typography") ],} satisfies Config;
export default config;
The new Tailwind 4 stylesheet
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@plugin "@tailwindcss/typography";@plugin "tailwindcss-animate";
:root { --background: hsl(0 0% 100%); --foreground: hsl(0 0% 3.9%); --card: hsl(0 0% 100%); --card-foreground: hsl(0 0% 3.9%); --popover: hsl(0 0% 100%); --popover-foreground: hsl(0 0% 3.9%); --primary: hsl(0 0% 9%); --primary-foreground: hsl(0 0% 98%); --secondary: hsl(0 0% 96%); --secondary-foreground: hsl(0 0% 9%); --muted: hsl(0 0% 96%); --muted-foreground: hsl(0 0% 45.1%); --accent: hsl(0 0% 96%); --accent-foreground: hsl(0 0% 9%); --destructive: hsl(0 84.2% 60.2%); --destructive-foreground: hsl(0 0% 98%); --border: hsl(0, 0%, 90%); --input: hsl(0 0% 89.8%); --ring: hsl(0 0% 3.9%); --radius: 8px;
--chart-1: hsl(12 76% 61%); --chart-2: hsl(173 58% 39%); --chart-3: hsl(197 37% 24%); --chart-4: hsl(43 74% 66%); --chart-5: hsl(27 87% 67%);
/* Shadcnblocks.com */ --muted-2: hsl(0 0% 90%); --muted-2-foreground: hsl(240 3.8% 46.1%);}.dark { --background: hsl(240 10% 3.9%); --foreground: hsl(0 0% 98%); --card: hsl(240 10% 3.9%); --card-foreground: hsl(0 0% 98%); --popover: hsl(240 10% 3.9%); --popover-foreground: hsl(0 0% 98%); --primary: hsl(0 0% 98%); --primary-foreground: hsl(240 5.9% 10%); --secondary: hsl(240 3.7% 15.9%); --secondary-foreground: hsl(0 0% 98%); --muted: hsl(240 3.7% 15.9%); --muted-foreground: hsl(240 5% 64.9%); --accent: hsl(240 3.7% 15.9%); --accent-foreground: hsl(0 0% 98%); --destructive: hsl(0 62.8% 30.6%); --destructive-foreground: hsl(0 0% 98%); --border: hsl(240 3.7% 15.9%); --input: hsl(240 3.7% 15.9%); --ring: hsl(240 4.9% 83.9%);
--chart-1: hsl(220 70% 50%); --chart-2: hsl(160 60% 45%); --chart-3: hsl(30 80% 55%); --chart-4: hsl(280 65% 60%); --chart-5: hsl(340 75% 55%);
/* Shadcnblocks.com */ --muted-2: hsl(0, 0%, 18%); --muted-2-foreground: hsl(240, 2%, 75%);}
@theme { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring);
--color-chart-1: hsl(12 76% 61%); --color-chart-2: hsl(173 58% 39%); --color-chart-3: hsl(197 37% 24%); --color-chart-4: hsl(43 74% 66%); --color-chart-5: hsl(27 87% 67%);
/* Shadcnblocks.com extra colors */ --color-muted-2: var(--muted-2); --color-muted-2-foreground: var(--muted-2-foreground); --color-transparent: transparent;
/* theme overrides */ --radius-xs: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px);
--breakpoint-sm: 640px; --breakpoint-md: 768px; --breakpoint-lg: 1024px; --breakpoint-xl: 1280px; --breakpoint-2xl: 1400px; /* default is 1536px */
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; --animate-fade-in-out: fade-in-out 4s ease-in-out;
@keyframes accordion-down { from { height: 0; } to { height: var(--radix-accordion-content-height); } } @keyframes accordion-up { from { height: var(--radix-accordion-content-height); } to { height: 0; } } @keyframes fade-in-out { 0% { opacity: 0; } 20% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } }}
@utility container { margin-inline: auto; padding-inline: 2rem;}
@layer components { button { cursor: pointer; } [class*="border"] { @apply border-border; } p { @apply text-muted-foreground; }}
@layer base { body { @apply bg-background text-foreground; }}
Let’s breakdown the steps we took.
In Tailwind 3, the hsl()
color function needed to be used inside the tailwind.config.js
. Which meant that in your stylesheet the Shadcn css variables needed to defined without the color space function, ie in the css they would look like this --background: 0 0% 100%;
instead of hsl(--background: 0 0% 100%);
Which was really frustrating, because it meant the vscode highlighting and vscode color picker didn’t work with Shadcn theming. It also broke the color picker for css variables in chrome dev tools! It seems like this wasnt a big deal for some people, but it was almost a deal breaker for me, its difficult enough already to remember what colors the shadcn theme color variables are 😅. A workaround was to declare the css variable with the full hsl color function in the css but If you use the color function in CSS you won’t be able to do opacity modifiers like bg-background/50
.
But anyway, in Tailwind 4, we once again wrap the color variables in hsl() inside the CSS file. Which solves the above problems!
You will need to either modify the the Shadcn CSS variable names to start with --color
OR you can opt to declare the Shadcn theme variables as provided in the :root
and re-map them to Tailwind compatible variable names inside of @theme
. We opted for the second approach as it keeps a clear seperation with the Shadn theme variables as provided, and our projects variables. in theory you could also move the shadn variables to their own CSS file like shadcn.css
and then import that for even clearer seperation ie
The Shadcn radius variable isnt a color, so we dont remap that to --color-radius
instead we update it to work with the new Taliwind radius values
@import "tailwindcss";
@theme { ...
--radius-xs: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px);}
The syntax for the keyframes can be touchy, you mostly dont need to use quotes around the keys or values.
@import "tailwindcss";
@theme { ...
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; --animate-fade-in-out: fade-in-out 4s ease-in-out;
@keyframes accordion-down { from { height: 0; }
to { height: var(--radix-accordion-content-height); } }
@keyframes accordion-up { from { height: var(--radix-accordion-content-height); }
to { height: 0; } }
@keyframes fade-in-out { 0%: { opacity: 0; } 20%: { opacity: 1; } 80%: { opacity: 1; } 100%: { opacity: 0; } }}
Here’s a complete list of the theme variables included by default https://tailwindcss.com/docs/theme#default-theme-variable-reference
For example if you had defined a custom class font-display
in Tailwind 3 like so.
const config = { ... theme: { extend: { fontFamily: { display: ["Inter",'ui-sans-serif','system-ui',], }, } } }
It would be mapped to a CSS variable like this in Tailwind 4.
@theme { .. --font-display: ["Inter",'ui-sans-serif','system-ui',]}
.container
classWe prefer to use a centered container. In Tailwind 3 you could specify the centered option.
const config = { theme: { container: { center: true, }, },}
In Tailwind 4 you need to extend the default container class this using the @utility
function.
@utility container { margin-inline: auto; padding-inline: 2rem;}
Tailwind 4 made a few changes
Which means in Shadcn we need to reset these (at least for the time being)
@layer components { button { cursor: pointer; } [class*="border"] { @apply border-border; }}
Final Tailwind 4 CSS with shadcn/ui themeing example
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@plugin "@tailwindcss/typography";@plugin "tailwindcss-animate";
:root { --background: hsl(0 0% 100%); --foreground: hsl(0 0% 3.9%); --card: hsl(0 0% 100%); --card-foreground: hsl(0 0% 3.9%); --popover: hsl(0 0% 100%); --popover-foreground: hsl(0 0% 3.9%); --primary: hsl(0 0% 9%); --primary-foreground: hsl(0 0% 98%); --secondary: hsl(0 0% 96%); --secondary-foreground: hsl(0 0% 9%); --muted: hsl(0 0% 96%); --muted-foreground: hsl(0 0% 45.1%); --accent: hsl(0 0% 96%); --accent-foreground: hsl(0 0% 9%); --destructive: hsl(0 84.2% 60.2%); --destructive-foreground: hsl(0 0% 98%); --border: hsl(0, 0%, 90%); --input: hsl(0 0% 89.8%); --ring: hsl(0 0% 3.9%); --radius: 8px; --chart-1: hsl(12 76% 61%); --chart-2: hsl(173 58% 39%); --chart-3: hsl(197 37% 24%); --chart-4: hsl(43 74% 66%); --chart-5: hsl(27 87% 67%);
--sidebar: hsl(0 0% 98%); --sidebar-foreground: hsl(240 5.3% 26.1%); --sidebar-primary: hsl(240 5.9% 10%); --sidebar-primary-foreground: hsl(0 0% 98%); --sidebar-accent: hsl(240 4.8% 95.9%); --sidebar-accent-foreground: hsl(240 5.9% 10%); --sidebar-border: hsl(220 13% 91%); --sidebar-ring: hsl(217.2 91.2% 59.8%);
/* Shadcnblocks.com */ --muted-2: hsl(0 0% 90%); --muted-2-foreground: hsl(240 3.8% 46.1%);}.dark { --background: hsl(240 10% 3.9%); --foreground: hsl(0 0% 98%); --card: hsl(240 10% 3.9%); --card-foreground: hsl(0 0% 98%); --popover: hsl(240 10% 3.9%); --popover-foreground: hsl(0 0% 98%); --primary: hsl(0 0% 98%); --primary-foreground: hsl(240 5.9% 10%); --secondary: hsl(240 3.7% 15.9%); --secondary-foreground: hsl(0 0% 98%); --muted: hsl(240 3.7% 15.9%); --muted-foreground: hsl(240 5% 64.9%); --accent: hsl(240 3.7% 15.9%); --accent-foreground: hsl(0 0% 98%); --destructive: hsl(0 62.8% 30.6%); --destructive-foreground: hsl(0 0% 98%); --border: hsl(240 3.7% 15.9%); --input: hsl(240 3.7% 15.9%); --ring: hsl(240 4.9% 83.9%);
--chart-1: hsl(220 70% 50%); --chart-2: hsl(160 60% 45%); --chart-3: hsl(30 80% 55%); --chart-4: hsl(280 65% 60%); --chart-5: hsl(340 75% 55%);
/* Shadcnblocks.com */ --muted-2: hsl(0, 0%, 18%); --muted-2-foreground: hsl(240, 2%, 75%);}
@theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring);
--color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5);
/* Shadcnblocks.com */ --color-muted-2: var(--muted-2); --color-muted-2-foreground: var(--muted-2-foreground); --color-transparent: transparent;
--radius-xs: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px);
--breakpoint-sm: 640px; --breakpoint-md: 768px; --breakpoint-lg: 1024px; --breakpoint-xl: 1280px; --breakpoint-2xl: 1400px; /* default is 1536px */
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; --animate-fade-in-out: fade-in-out 4s ease-in-out; --animate-progress: progress 8s linear; --animate-infinite-slider: infiniteSlider 20s linear infinite; --animate-infinite-slider-reverse: infiniteSliderReverse 20s linear infinite; --animate-shadow-ping: shadow-ping 1.5s ease-in-out infinite; --animate-flip-btn: flip-btn 6s infinite steps(2, end); --animate-rotate-btn: rotate-btn 3s linear infinite both; --animate-light-to-right-top: light-to-right 4s linear infinite; --animate-light-to-right-bottom: light-to-right 4s linear infinite; --animate-marquee: marquee 25s linear infinite; --animate-slide-to-right: slide-to-right 3s linear infinite; --animate-slide-to-top: slide-to-top 3s linear infinite;
@keyframes accordion-down { from { height: 0; } to { height: var(--radix-accordion-content-height); } }
@keyframes accordion-up { from { height: var(--radix-accordion-content-height); } to { height: 0; } }
@keyframes fade-in-out { 0% { opacity: 0; } 20% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } }
@keyframes progress { from { width: "0%"; } to { width: "100%"; } }
@keyframes infiniteSlider { 0% { transform: "translateX(0)"; } 100% { transform: "translateX(calc(-250px * 5))"; } }
@keyframes infiniteSliderReverse { 0% { transform: "translateX(calc(-250px * 5))"; } 100% { transform: "translateX(0)"; } }
@keyframes fade-in-scale { 0% { opacity: 0; transform: scale(0.95); } 100% { opacity: 1; transform: scale(1); } }
@keyframes transform1 { 0%, 25%, 100% { width: 100%; padding-bottom: 120%; } 33.33%, 58.33% { width: 0%; padding-bottom: 0%; } 66.66%, 91.66% { width: 90%; padding-bottom: 100%; } }
@keyframes transform2 { 0%, 25%, 100% { width: 65%; padding-bottom: 65%; } 33.33%, 58.33% { width: 95%; padding-bottom: 114%; } 66.66%, 91.66% { width: 52%; padding-bottom: 52%; } }
@keyframes transform3 { 0%, 25%, 100% { width: 78%; padding-bottom: 100%; } 33.33%, 58.33% { width: 78%; padding-bottom: 94%; } 66.66%, 91.66% { width: 95%; padding-bottom: 76%; } }
@keyframes transform4 { 0%, 25%, 66.66%, 91.66%, 100% { width: 0%; padding-bottom: 0%; } 33.33%, 58.33% { width: 65%; padding-bottom: 46%; } }
@keyframes image1 { 0%, 25%, 100% { opacity: 1; } 33.33%, 58.33%, 66.66%, 91.66% { opacity: 0; } }
@keyframes image2 { 0%, 25%, 33.33%, 58.33%, 100% { opacity: 0; } 66.66%, 91.66% { opacity: 1; } }
@keyframes image3 { 0%, 25%, 66.66%, 91.66%, 100% { opacity: 0; } 33.33%, 58.33% { opacity: 1; } }
@keyframes gradient-spin { 0% { transform: translateX(-50%) translateY(-50%) rotate(0deg); } 100% { transform: translateX(-50%) translateY(-50%) rotate(360deg); } }
@keyframes shadow-ping { 0% { boxshadow: 0 0 0 0px theme("colors.neutral.100"); } 50% { boxshadow: 0 0 0 12px theme("colors.neutral.300"); } 100% { boxshadow: 0 0 0 12px transparent; } }
@keyframes show-text { 0%, 14.28% { opacity: 0; } 17%, 26% { opacity: 1; } 28.58%, 100% { opacity: 0; } }
@keyframes flip-btn { to { transform: rotate(360deg); } }
@keyframes rotate-btn { to { transform: rotate(90deg); } }
@keyframes slide-to-right { 0% { opacity: 0; left: 0; } 50% { opacity: 1; } 100% { opacity: 0; left: 80%; } }
@keyframes slide-to-top { 0% { opacity: 0; bottom: 0; } 50% { opacity: 1; } 100% { opacity: 0; bottom: 80%; } }
@keyframes light-to-right { 0% { transform: translate(0%); opacity: 0; } 50% { opacity: 1; } 100% { transform: translate(100%); opacity: 0; } }
@keyframes marquee { 0% { transform: translateX(0%); } 100% { transform: translateX(-100%); } }}
@utility container { margin-inline: auto; padding-inline: 2rem;}
@layer components { button { cursor: pointer; } [class*="border"] { @apply border-border; } p { @apply text-muted-foreground; }}