Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.
Author
Paul Popus
Published On

How to setup Tailwind CSS and shadcn/ui in Payload

Author
Paul Popus
Published On
Setup Payload with Tailwind CSS and shadcin/ui
Setup Payload with Tailwind CSS and shadcin/ui

One of the most requested tutorials we are asked for is how to integrate Tailwind CSS, the still-contentious CSS utility framework that warrants no introduction, into the Payload admin panel.

As of 3.0, the Payload admin panel integrates directly into your Next.js App Router project. This lends us the flexibility and some of the integrations Next.js already comes with, making the addition of Tailwind or other libraries easier than in previous versions.

Setup

Step 0 is to have Payload installed using this command:

1
npx create-payload-app@beta

From here on we refer to the official installation guide, so let's add the Tailwind dependencies.

1
npm install -D tailwindcss postcss autoprefixer

And the init command can do the rest:

1
npx tailwindcss init -p

Now let's edit the generated tailwind.config.js to add the content configuration for our files.

1
/** @type {import('tailwindcss').Config} */
2
export default {
3
content: ['./src/**/*.{jsx,tsx}'], // tell tailwind where to look
4
theme: {
5
extend: {},
6
},
7
plugins: [],
8
}

Lastly we need to import Tailwind directives into a global stylesheet for our frontend, let's create a globals.css file next to our layout in (app).

1
@tailwind base;
2
@tailwind components;
3
@tailwind utilities;

And we need to make sure this is imported in our layout.tsx component.

1
import { ReactNode } from 'react'
2
3
type LayoutProps = {
4
children: ReactNode
5
}
6
7
import './globals.css'
8
9
const Layout = ({ children }: LayoutProps) => {
10
return (
11
<html>
12
<body>
13
{children}
14
</body>
15
</html>
16
)
17
}
18
19
export default Layout

From here on, you can use it in your frontend as you need, but we're also going to setup a custom component for Payload and use it there.

Usage in the admin panel and custom components

We're going to setup a simple UI field in Payload but any custom component will work; in our collection let's add the field.

1
import AlertBox from '@/components/AlertBox'
2
import type { CollectionConfig } from 'payload/types'
3
4
export const Posts: CollectionConfig = {
5
slug: 'posts',
6
admin: {
7
useAsTitle: 'title',
8
},
9
fields: [
10
{
11
name: 'title',
12
type: 'text',
13
},
14
{
15
name: 'alertBox',
16
type: 'ui',
17
admin: {
18
components: {
19
Field: AlertBox,
20
},
21
},
22
},
23
],
24
}

And in /components/AlertBox.tsx we'll create a simple placeholder component.

1
import React from 'react'
2
3
const AlertBox: React.FC = () => {
4
return <div className="p-4 border-4 border-solid border-yellow-100">Please add a title.</div>
5
}
6
7
export default AlertBox

You won't be seeing the styles updated just yet as we need to add the Tailwind directives to our admin panel's CSS as well. So locate custom.scss in your (payload) route group and we'll add the same @tailwind directives in there

1
// @tailwind base; <- do not add
2
@tailwind components;
3
@tailwind utilities;

Do not add the base styles here as they contain a lot of style resets that may interfere with the admin panel.

And now you can use Tailwind in any of your components! Open your admin panel and go to the collection using your AlertBox UI field to see it in action.

Bonus: shadcn/ui

Shadcn/ui is a library of components to be installed and used locally and it uses Tailwind CSS so please make sure it's working before running the following command

1
npx shadcn-ui@latest init

This will take you through the installation steps, and pretty much all aspects can be left as the default however you need to make sure that the globals.css path is configured to your actual file's inside (app).

This will generate and override certain files for you including the tailwind.config.js and globals.css files.

Now let's add the Inter font for shadcn but you can replace this with any font that you want. Our layout.tsx in (app) should look something like this

1
import { ReactNode } from 'react'
2
import { cn } from '@/lib/utils'
3
import { Inter as FontSans } from 'next/font/google'
4
5
type LayoutProps = {
6
children: ReactNode
7
}
8
9
import './globals.css'
10
11
const fontSans = FontSans({
12
subsets: ['latin'],
13
variable: '--font-sans',
14
})
15
16
const Layout = ({ children }: LayoutProps) => {
17
return (
18
<html>
19
<body className={cn('min-h-screen bg-background font-sans antialiased', fontSans.variable)}>
20
{children}
21
</body>
22
</html>
23
)
24
}
25
26
export default Layout

Since the installation process sets up everything for us, we need to make a few more adjustments to work with Payload's admin panel.

In tailwind.config.js we need to adjust the dark mode selector to include data-theme which is what Payload uses instead of a class.

1
darkMode: ['selector', '[data-theme="dark"]', '.dark'],

And in (payload)/custom.scss we need to copy over the generated values for root: CSS variables so that the shadcn components can utilise the styles out of the box.

1
@tailwind components;
2
@tailwind utilities;
3
4
:root {
5
--background: 0 0% 100%;
6
--foreground: 222.2 84% 4.9%;
7
8
--card: 0 0% 100%;
9
--card-foreground: 222.2 84% 4.9%;
10
11
--popover: 0 0% 100%;
12
--popover-foreground: 222.2 84% 4.9%;
13
14
--primary: 222.2 47.4% 11.2%;
15
--primary-foreground: 210 40% 98%;
16
17
--secondary: 210 40% 96.1%;
18
--secondary-foreground: 222.2 47.4% 11.2%;
19
20
--muted: 210 40% 96.1%;
21
--muted-foreground: 215.4 16.3% 46.9%;
22
23
--accent: 210 40% 96.1%;
24
--accent-foreground: 222.2 47.4% 11.2%;
25
26
--destructive: 0 84.2% 60.2%;
27
--destructive-foreground: 210 40% 98%;
28
29
--border: 214.3 31.8% 91.4%;
30
--input: 214.3 31.8% 91.4%;
31
--ring: 222.2 84% 4.9%;
32
33
--radius: 0.5rem;
34
}
35
36
[data-theme='dark'] {
37
--background: 222.2 84% 4.9%;
38
--foreground: 210 40% 98%;
39
40
--card: 222.2 84% 4.9%;
41
--card-foreground: 210 40% 98%;
42
43
--popover: 222.2 84% 4.9%;
44
--popover-foreground: 210 40% 98%;
45
46
--primary: 210 40% 98%;
47
--primary-foreground: 222.2 47.4% 11.2%;
48
49
--secondary: 217.2 32.6% 17.5%;
50
--secondary-foreground: 210 40% 98%;
51
52
--muted: 217.2 32.6% 17.5%;
53
--muted-foreground: 215 20.2% 65.1%;
54
55
--accent: 217.2 32.6% 17.5%;
56
--accent-foreground: 210 40% 98%;
57
58
--destructive: 0 62.8% 30.6%;
59
--destructive-foreground: 210 40% 98%;
60
61
--border: 217.2 32.6% 17.5%;
62
--input: 217.2 32.6% 17.5%;
63
--ring: 212.7 26.8% 83.9%;
64
}

We do this by copying the generated code from globals.css but note that we won't be using base here so we can remove the @layer base directives and we need to replace the .dark selector with a [data-theme=&#39;dark&#39;] selector for the admin panel.

That's a lot of code. o please check out the example repo for a reference.

Now we can add the alert component, this will install it in a ui directory under components

1
npx shadcn-ui@latest add alert

And we can go back to our AlertBox component and let's use this new alert instead. Import the dependencies

1
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"

And we'll use it as is.

1
<Alert>
2
<AlertTitle>Heads up!</AlertTitle>
3
<AlertDescription>
4
Please add an appropriate title.
5
</AlertDescription>
6
</Alert>

the final component for AlertBox will look like this:

1
import React from 'react'
2
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
3
4
const AlertBox: React.FC = () => {
5
return (
6
<Alert>
7
<AlertTitle>Heads up!</AlertTitle>
8
<AlertDescription>Please add an appropriate title.</AlertDescription>
9
</Alert>
10
)
11
}
12
13
export default AlertBox

And that should be it, you've now got the full power of shadcn components in Payload, follow along with the full example.

Combine it with our React hooks to read and manipulate data in your fields!