


Vincent is the founder and director of Rubix Studios, with over 20 years of experience in branding, marketing, film, photography, and web development. He is a certified partner with industry leaders including Google, Microsoft, AWS, and HubSpot. Vincent also serves as a member of the Maribyrnong City Council Business and Innovation Board and is undertaking an Executive MBA at RMIT University.
TailwindCSS can be used effectively with PayloadCMS, its global styling behaviour requires a structured setup to prevent unnecessary overrides in the admin panel. This guide presents a clear method for enabling TailwindCSS v4 across both the application frontend and the Payload admin, along with support for shadcn/ui components.
A new Payload project may be created using the standard scaffolding process, or TailwindCSS can be added to an existing codebase. The initial steps focus on installing TailwindCSS and configuring PostCSS so styles are compiled correctly.
Tailwind installation
Install tailwindcss, @tailwindcss/postcss, and postcss via npm.
bashnpm install tailwindcss @tailwindcss/postcss postcss
postcss.config.mjs
Add @tailwindcss/postcss to your postcss.config.mjs file, or wherever PostCSS is configured in your project.
javascript/** @type {import('postcss-load-config').Config} */const config = {plugins: {'@tailwindcss/postcss': {},},}export default config
Ensure there are separation between your application's frontend and PayloadCMS to avoid style overrides.
(frontend)/layout.tsx
For NextJS frontends, tailwind import can simply be done within the root layout.
typescriptimport { Inter } from 'next/font/google'import '@/styles/globals.css'const inter = Inter({adjustFontFallback: true,display: 'swap',preload: true,subsets: ['latin'],variable: '--font-inter',})export default async function RootLayout({children,}: Readonly<{children: ReactNode}>) {return (<html data-scroll-behavior="smooth" lang="en" suppressHydrationWarning><body className={`${inter.variable} bg-background text-foreground font-sans antialiased`}>{children}</body></html>)}
PayloadCMS however, as per the warning described within the layout.tsx file.
typescript/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. *//* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
We should not import tailwind in the same way as the frontend. Alternatively, we should utilise the standard custom.scss provided. This is a standard file that PayloadCMS produces for the purpose of CSS manipulation within Payload and therefore safe to edit.
app/(payload)/custom.scss
The following code can simply be copied into custom.scss.
typescript@layer theme {@import 'tailwindcss/theme.css';}@layer base {.twp {@import 'tailwindcss/preflight.css';}.twp.no-twp {*,::after,::before,::backdrop,::file-selector-button {all: revert-layer;}}}@layer components;@layer utilities {@import 'tailwindcss/utilities.css';}
In many cases, you'll notice the collections table being incorrectly styled. To address this, adding in this additional code below the tailwind configuration in custom.scss will address this issue.
typescript.table.table--appearance-default > div:first-of-type {width: 100% !important;}

The admin panel uses its own styling structure, so Tailwind should not be applied globally. A scoped approach allows Tailwind to be used only where needed. A .twp class enables Tailwind within selected components, while .no-twp can be used to prevent style inheritance in specific areas.
This ensures the admin panel maintains its standard appearance while still allowing Tailwind utilities to be used in custom admin views.
typescript'use client'import { useAuth, useConfig } from '@payloadcms/ui'import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'import {DropdownMenu,DropdownMenuContent,DropdownMenuLabel,DropdownMenuSeparator,DropdownMenuTrigger,} from '@/components/ui/dropdown-menu'import { Skeleton } from '@/components/ui/skeleton'import { Link } from '@/lib/transition'import { type User } from '@/payload/payload-types'import { getMediaUrl } from '@/utils/isMedia'export const ProfilePicture = () => {const { user } = useAuth<User>()const { config } = useConfig()if (!user) {return <Skeleton className="twp h-10 w-10 animate-pulse rounded-full" />}const { avatar, fname, lname } = userconst avatarurl = getMediaUrl(avatar)return (<DropdownMenu><DropdownMenuTrigger className="twp cursor-pointer border-0 bg-transparent p-0 ring-0 focus:border-0 focus:ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-offset-0"><Avatar className="top-1 size-8">{avatarurl ? (<AvatarImagewidth={20}src={avatarurl}alt={`${fname} ${lname}`}height={20}className="select-none"fetchPriority="high"/>) : (<AvatarFallback>{fname?.[0]} {lname?.[0]}</AvatarFallback>)}</Avatar></DropdownMenuTrigger><DropdownMenuContent className="twp z-50 min-w-32 gap-0 space-y-0 overflow-hidden rounded-lg border border-(--theme-elevation-150) bg-(--theme-bg) p-0 text-foreground dark:text-background"><DropdownMenuLabel className="p-1"><Linkhref={`${config.serverURL}/admin/account`}onClick={(e) => e.stopPropagation()}className="m-0 block w-full rounded-t-lg px-2.5 py-4.5 text-left leading-0 no-underline transition-all duration-500 hover:bg-(--theme-elevation-50)">Account</Link></DropdownMenuLabel><DropdownMenuSeparator className="bg-(--theme-elevation-150) p-0" /><DropdownMenuLabel className="p-1"><Linkhref={`${config.serverURL}/admin/logout`}onClick={(e) => e.stopPropagation()}className="m-0 block w-full rounded-b-lg px-2.5 py-4.5 text-left leading-0 no-underline transition-all duration-500 hover:bg-(--theme-elevation-50)">Logout</Link></DropdownMenuLabel></DropdownMenuContent></DropdownMenu>)}
Lets take this for example, we're utilising Payload's native useAuth and useConfig to obtain our user state and payload configuration to pass into our component.
Our child components are imported from shadcn/ui's library. within our example <DropdownMenu> itself does not expose a className prop and therefore we've placed our .twp class within it's child, every child there are inherits tailwind unless .no-twp is added.
typescript<DropdownMenuTrigger className="twp cursor-pointer border-0 bg-transparent p-0 ring-0 focus:border-0 focus:ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-offset-0" />
TailwindCSS v4 introduces a revised layering system that works well with Payload as long as the order of theme, base, components, and utilities is consistent. The configuration added to the admin stylesheet ensures Tailwind loads only within scoped regions and does not override Payload defaults.
app/(payload)/custom.scss
css@layer theme {@import 'tailwindcss/theme.css';}@layer base {.twp {@import 'tailwindcss/preflight.css';}.twp.no-twp {*,::after,::before,::backdrop,::file-selector-button {all: revert-layer;}}}@layer components;@layer utilities {@import 'tailwindcss/utilities.css';}
shadcn/ui global.css
Shadcn/ui provides a global style sheet example, this style sheet utilises .dark for light and dark mode coordination.
global.css
In our example we've broken global.css into smaller portions for maintainability and utilisation.
typescript@import 'tailwindcss';@import 'tw-animate-css';@import './animation.css';@import './base.css';@import './components.css';@import './utilities.css';
Assuming shadcn/ui's styling is within base.css
app/(payload)/custom.scss
We can import a shared base.css file that contains shadcn's native styling.
css@import "../../styles/base.css";@layer theme {@import 'tailwindcss/theme.css';}@layer base {.twp {@import 'tailwindcss/preflight.css';}.twp.no-twp {*,::after,::before,::backdrop,::file-selector-button {all: revert-layer;}}}@layer components;@layer utilities {@import 'tailwindcss/utilities.css';}
styles/base.css
PayloadCMS uses [data-theme='dark'] for dark style while shadcn/ui uses .dark. To ensure compatibility between shared styling we've modified @custom-variant to accomodate both frontend and Payload backend.
css@custom-variant dark (&:is(.dark *, [data-theme='dark'] *));:root {--background: oklch(1 0 0);--foreground: oklch(0.145 0 0);--card: oklch(1 0 0);--card-foreground: oklch(0.145 0 0);--popover: oklch(1 0 0);--popover-foreground: oklch(0.145 0 0);--primary: oklch(0.205 0 0);--primary-foreground: oklch(0.985 0 0);--secondary: oklch(0.97 0 0);--secondary-foreground: oklch(0.205 0 0);--muted: oklch(0.97 0 0);--muted-foreground: oklch(0.556 0 0);--accent: oklch(0.97 0 0);--accent-foreground: oklch(0.205 0 0);--destructive: oklch(0.577 0.245 27.325);--destructive-foreground: oklch(0.577 0.245 27.325);--border: oklch(0.922 0 0);--input: oklch(0.922 0 0);--ring: oklch(0.708 0 0);--chart-1: oklch(0.646 0.222 41.116);--chart-2: oklch(0.6 0.118 184.704);--chart-3: oklch(0.398 0.07 227.392);--chart-4: oklch(0.828 0.189 84.429);--chart-5: oklch(0.769 0.188 70.08);--radius: 0.625rem;--sidebar: oklch(0.985 0 0);--sidebar-foreground: oklch(0.145 0 0);--sidebar-primary: oklch(0.205 0 0);--sidebar-primary-foreground: oklch(0.985 0 0);--sidebar-accent: oklch(0.97 0 0);--sidebar-accent-foreground: oklch(0.205 0 0);--sidebar-border: oklch(0.922 0 0);--sidebar-ring: oklch(0.708 0 0);}.dark {--background: oklch(0.145 0 0);--foreground: oklch(0.985 0 0);--card: oklch(0.145 0 0);--card-foreground: oklch(0.985 0 0);--popover: oklch(0.145 0 0);--popover-foreground: oklch(0.985 0 0);--primary: oklch(0.985 0 0);--primary-foreground: oklch(0.205 0 0);--secondary: oklch(0.269 0 0);--secondary-foreground: oklch(0.985 0 0);--muted: oklch(0.269 0 0);--muted-foreground: oklch(0.708 0 0);--accent: oklch(0.269 0 0);--accent-foreground: oklch(0.985 0 0);--destructive: oklch(0.396 0.141 25.723);--destructive-foreground: oklch(0.637 0.237 25.331);--border: oklch(0.269 0 0);--input: oklch(0.269 0 0);--ring: oklch(0.556 0 0);--chart-1: oklch(0.488 0.243 264.376);--chart-2: oklch(0.696 0.17 162.48);--chart-3: oklch(0.769 0.188 70.08);--chart-4: oklch(0.627 0.265 303.9);--chart-5: oklch(0.645 0.246 16.439);--sidebar: oklch(0.205 0 0);--sidebar-foreground: oklch(0.985 0 0);--sidebar-primary: oklch(0.488 0.243 264.376);--sidebar-primary-foreground: oklch(0.985 0 0);--sidebar-accent: oklch(0.269 0 0);--sidebar-accent-foreground: oklch(0.985 0 0);--sidebar-border: oklch(0.269 0 0);--sidebar-ring: oklch(0.439 0 0);}@theme inline {--font-sans: var(--font-inter);--font-mono: var(--font-inter);--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);--radius-sm: calc(var(--radius) - 4px);--radius-md: calc(var(--radius) - 2px);--radius-lg: var(--radius);--radius-xl: calc(var(--radius) + 4px);--color-sidebar: var(--sidebar);--color-sidebar-foreground: var(--sidebar-foreground);--color-sidebar-primary: var(--sidebar-primary);--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);--color-sidebar-accent: var(--sidebar-accent);--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);--color-sidebar-border: var(--sidebar-border);--color-sidebar-ring: var(--sidebar-ring);}
This configuration provides a clean and predictable way to use TailwindCSS and shadcn/ui within both the frontend and the Payload admin panel. By scoping Tailwind in the admin and aligning its theme and base layers, developers gain consistent styling without disrupting Payload’s built-in interface. The approach supports scalable component development and improves the speed and flexibility of UI work across the entire project.
Vincent is the founder and director of Rubix Studios, with over 20 years of experience in branding, marketing, film, photography, and web development. He is a certified partner with industry leaders including Google, Microsoft, AWS, and HubSpot. Vincent also serves as a member of the Maribyrnong City Council Business and Innovation Board and is undertaking an Executive MBA at RMIT University.