adrianmaj.com

Payload,  Untitled category,  Next.js

Integrating TailwindCSS with PayloadCMS - Complete Guide

Author

Adrian Maj

Date published

PayloadCMS + TailwindCSS

TailwindCSS is a tool that enables quick and efficient component styling using utility-classes, significantly speeding up the development process. It also works great in applications based on PayloadCMS, however, its integration, especially with the admin panel (which is based on SASS), isn't as intuitive and simple as it might seem. The simplest approach of throwing Tailwind styles into the custom.scss file leads to unwanted effects in the panel (mainly by overwriting Payload's default styles), so it's worth doing it a bit differently – as you're about to find out.

In this guide, I'll show you how to properly configure TailwindCSS in a project with PayloadCMS, so that it works seamlessly in both the front-end layer of the application and the admin panel. You'll also learn how to efficiently implement shadcn/ui for components in the Payload panel – and beyond.

⚠️ Warning: In this guide, we're working with the latest version at the time of writing - v4.0. The configuration in version v3.x.x differs in how styles are scoped. You'll find the details below.

Project Configuration

The first step is creating a project using npx create-payload-app. If you're implementing TailwindCSS into an existing project, skip this step.
💡 Tip: If you're just starting your journey with PayloadCMS, check out my tutorial series - PayloadCMS from Scratch.

We move on to installing Tailwind, following the documentation, but focusing only on steps two and three.

We install TailwindCSS to our project using the command pnpm add tailwindcss @tailwindcss/postcss postcss

We create a postcss.config.mjs file in the project's root folder, and paste the following config into it:

1const config = {
2 plugins: {
3 '@tailwindcss/postcss': {},
4 },
5}
6export default config

According to the documentation, we now need to import TailwindCSS into our CSS file. We do this in the src/app/(frontend)/styles.css file because there are no styles there that could be overwritten by Tailwind. This way, we'll avoid conflicts.

In the admin panel, however, we don't want to overwrite Payload's default styles, so in the src/app/(payload)/custom.scss file, we need to modify the way styles are imported slightly. We create a twp class that we'll assign to elements that should use Tailwind, and a no-twp class to exclude elements that shouldn't be styled by Tailwind.

⚠️ Warning: This solution doesn't work in Tailwind v3. In v3, you should use the Tailwind Scoped Preflight plugin instead. You can find an example implementation with this plugin in this blog's source code.

Both files should look as follows: src/app/(frontend)/styles.css

1@import 'tailwindcss';

src/app/(payload)/custom.scss

1@layer theme {
2 @import 'tailwindcss/theme.css';
3}
4
5@layer base {
6 .twp {
7 @import 'tailwindcss/preflight.css';
8 }
9
10 .no-twp {
11 *,
12 ::after,
13 ::before,
14 ::backdrop,
15 ::file-selector-button {
16 all: revert-layer;
17 }
18 }
19}
20
21@layer components;
22
23@layer utilities {
24 @import 'tailwindcss/utilities.css';
25}

After pasting the code into custom.scss, the admin panel should remain in its original state – no default Payload styles should be overwritten. Just adding the TailwindCSS import in this file overwrites and breaks the default appearance, which is why it's necessary to scope Tailwind using the appropriate twp and no-twp classes (the class names are examples, you can change them).

Practical Application

From this point on, we can utilize the full potential of Tailwind in our application. While in the front-end layer of the application it's very simple - Tailwind is immediately ready to use, in Payload panel components, you need to add the twp class to the parent container so that all styles (including the default ones) are assigned correctly.

For the purposes of this guide, we'll create our own admin panel dashboard component. In our project, we create the file src/components/Dashboard/index.tsx and create a basic component.

1// src/components/Dashboard/index.tsx
2export const Dashboard = () => {
3 return <div>Dashboard</div>
4}

Then in the payload.config.ts file, we add the path to our component in the following way:

1export default buildConfig({
2 admin: {
3 // ... admin config
4 components: {
5 views: {
6 dashboard: {
7 Component: '@/components/Dashboard#Dashboard',
8 },
9 },
10 },
11 },
12 // ... rest of config
13})

At this point, on the admin panel's main page, instead of the standard collections view, we should see a small "Dashboard" text.

💡 Tip: If you encounter an error with this change, check the path correctness, and generate the importMap using the command pnpm generate:importmap.

Custom dashboard

After replacing the default dashboard with our own component, we gain full control over its appearance, behavior, and style. Tailwind's utility classes work immediately without additional modifications, however, base styles are not automatically applied to the component.

For some elements (e.g., <input />), Payload automatically applies its base styles, which can lead to conflicts – especially when using UI libraries like shadcn/ui. The solution is to apply the previously created .twp class to the parent container, which will override Payload's default styles with TailwindCSS base styles.

1export const Dashboard = () => {
2 return (
3 <div className="twp flex flex-col w-full items-center gap-8">
4 <h1 className="text-4xl">Dashboard</h1>
5 <input className="bg-slate-900 text-2xl px-2 py-1 rounded-md" />
6 </div>
7 )
8}

Here's the initially styled dashboard component. We can now freely modify all elements, and Payload's default styles (such as borders on inputs) have been correctly overwritten by Tailwind only in the created component, without affecting the entire admin panel.

Styled dashboard

Shadcn/ui components

Tailwind is a great foundation, but often we need ready-made UI components to speed up development. One of the most popular (and in my opinion, best) UI libraries based on Tailwind is shadcn/ui. Components from this library provide a solid base for building your own customized layouts – both in the Payload panel and in the user-facing part of the application.

An additional advantage of shadcn/ui is that components are generated directly in our project rather than being pulled from an external library. This gives us full control over their modifications, allowing us to fully adapt them to our needs and application style.

⚠️ Warning: The configuration discussed in this guide is for Tailwind v4. For v3, refer to the shadcn/ui docs.

To use shadcn/ui components, we first need to initialize the library. We open the terminal in our project and run the command pnpm dlx shadcn@canary init

Shadcn initialization

Depending on our preferences, we select the default color, and leave the CSS variables option at the default yes. After successful initialization, the terminal window should display the message: Success! Project initialization completed. You may now add components.

Successful shadcn/ui init

With the library initialized, we can add a sample component. You can find the list of components in the docs, however, keep in mind that currently, components adapted for Tailwind v4 need to be installed with the canary tag instead of latest, due to the recent update to v4. Let's add the Button component using the command pnpm dlx shadcn@canary add button

Adding button component from shadcn/ui

At this point, a button.tsx file with a button component will be created in the src/components/ui folder, however, currently the component will only display correctly in the application part from the src/app/(frontend) directory. This won't happen in the Payload panel because during initialization, shadcn placed its styles only in the styles.css file. The custom.scss file, which concerns the admin panel, wasn't populated with shadcn styles, and we need to do this manually, as well as apply some modifications. After opening the styles.css file, we can see that shadcn placed a lot of additional styles and variables there, the file should look something like this:

1@import 'tailwindcss';
2
3@plugin 'tailwindcss-animate';
4
5@custom-variant dark (&:is(.dark *));
6
7:root {
8 // ...variables
9}
10
11.dark {
12 // ...variables
13}
14
15@theme inline {
16 // ...variables
17}
18
19@layer base {
20 * {
21 @apply border-border outline-ring/50;
22 }
23 body {
24 @apply bg-background text-foreground;
25 }
26}

We copy the code from the @plugin 'tailwindcss-animate' directive up to the end of @theme inline and paste it at the very beginning of the custom.scss file, then we move the contents of @layer base from the styles.css file inside our twp class. This is how the completed custom.scss file should look:

1@plugin 'tailwindcss-animate';
2
3@custom-variant dark (&:is([data-theme='dark'] *));
4
5:root {
6 // ...variables
7}
8
9[data-theme='dark'] {
10 // ...variables
11}
12
13@theme inline {
14 // ...variables
15}
16
17@layer theme {
18 @import 'tailwindcss/theme.css';
19}
20
21@layer base {
22 .twp {
23 @import 'tailwindcss/preflight.css';
24
25 * {
26 @apply border-border outline-ring/50;
27 }
28 body {
29 @apply bg-background text-foreground;
30 }
31 }
32
33 .no-twp {
34 *,
35 ::after,
36 ::before,
37 ::backdrop,
38 ::file-selector-button {
39 all: revert-layer;
40 }
41 }
42}
43
44@layer components;
45
46@layer utilities {
47 @import 'tailwindcss/utilities.css';
48}
49

⚠️ Warning: In the @custom-variant directive, and in the second set of variables (previously under .dark), you need to replace the .dark class with [data-theme='dark']. This will allow the component styles to match the theme selected in the Payload panel.

Now we can test the functionality by adding the Button component to our dashboard.

1import { Button } from '../ui/button'
2
3export const Dashboard = () => {
4 return (
5 <div className="twp flex flex-col w-full items-center gap-8">
6 <h1 className="text-4xl">Dashboard</h1>
7 <input className="bg-slate-900 text-2xl px-2 py-1 rounded-md" />
8 <Button size="lg">Hello world</Button>
9 </div>
10 )
11}

Thanks to the changes made in the custom.scss file, our button should now correctly receive styles from shadcn/ui. Moreover, it will react to theme changes – both when switching from dark to light and vice versa. However, remember that components should be placed inside a parent container with the twp class assigned for default styles to be applied correctly.

Shadcn button in custom dashboard

⚠️ Warning: Components from shadcn/ui in the admin panel may have a smaller font size because Payload sets the font size to 13px instead of the default 16px. You can adjust the font size using Tailwind's text-[xx] classes, for example.

Summary

In this guide, I showed you how to effectively integrate TailwindCSS with PayloadCMS and enhance the project with a UI library. The presented configuration helps avoid typical problems with conflicting Payload styles and enables quick and efficient creation of custom Tailwind-based components. This solution provides a solid foundation for building visually consistent and functional applications.

You can find the source code with examples on my GitHub as usual. If you liked this post, check out my other entries as well. I also encourage you to follow my social media profiles to not miss any new Payload tutorials.