How to Use Shadcn with Next.js 15
Shadcn/ui is a modern UI component library built for React developers. It provides a collection of customizable, accessible, and themeable components designed to seamlessly integrate with Tailwind CSS. The main goal of shadcn/ui is to help developers quickly create consistent, visually appealing user interfaces while maintaining full control over styling and functionality.
With built-in accessibility, theme support, and compatibility with Tailwind’s utility classes, shadcn/ui is an excellent choice for building scalable and maintainable applications.
Overview
In this guide, we’ll walk through the steps to integrate shadcn/ui with Next.js to build accessible and customizable UI components.
- What is shadcn/ui?
- How to install Next.js and Shadcn
- Using shadcn/ui components
- How to customizing shadcn/ui components
- Conclusion
What is shadcn/ui?
Shadcn/ui is a UI component library designed for React developers. It offers a set of components that are not only customizable but also accessible and themeable, making it easy to create consistent, attractive interfaces. The library is built to work seamlessly with Tailwind CSS, allowing developers to use utility-first classes while maintaining full control over the UI's styling and behavior.
It provides essential features like accessibility support and the ability to easily theme your application, making it a great choice for building modern, scalable user interfaces.
How to Install Next.js and Shadcn
I will be following the official installation guide provided in the Shadcn documentation. Feel free to follow along with the steps outlined in the documentation.
First, you'll need to select the framework you're gonna use. For the purpose of this guide, I will be focusing on Next.js. Therefore, choose the Next.js option in the documentation to proceed with the installation steps specific to that framework.

Now, you need to install and configure a new Next.js project.
How to install Next.js
You can use your preferred package manager; just follow the respective command for your chosen tool below. For more details, refer to the Next.js documentation.
I'll be using pnpm
because Next.js 15 is built with React 19 (rc), and to fully support it, npm
requires an extra step to install Shadcn. For more information on this, check the official documentation if you're using npm
.
To Install Next.js with npm:
npx create-next-app@latest
To Install Next.js with pnpm:
pnpm create next-app@latest
To Install Next.js with yarn:
yarn create next-app
After running one of those commands, you'll see the following configuration options:

- I chose TypeScript because shadcn/ui integrates well with it, providing better type safety and a smoother development experience.
- I selected the
/src
directory because I prefer this organizational pattern. It helps keep the project structure clean and scalable, especially as the project grows. - I opted for the App Router since it's the most efficient way to develop a Next.js project. It uses a folder-based approach to organize pages, which aligns with modern development practices.
- I chose not to use Turbopack to avoid potential instability or compatibility issues that might arise while using this new bundler, especially in the early stages of development.
- I kept the default import alias (
@/*
) because I like how it works. The@
alias simplifies imports and makes it easier to navigate the project's structure.
Now you are ready to install shadcn.
How to install Shadcn
Before continuing, don't forget to navigate to the project folder you created at the previous step.
cd [your-folder-name]
To install shadcn with npm:
npx shadcn@latest init -d
To install shadcn with pnpm:
pnpm dlx shadcn@latest init -d
To install shadcn with yarn:
npx shadcn@latest init -d
For some reason, using Shadcn with Next.js 15 and the package managers pnpm
or yarn
causes the configuration step for components.json
to be done automatically with the following options:
- style: "new-york"
- baseColor: "zinc"
- cssVariables: true
You can check the available options for style
and baseColor
in the components.json
documentation.
Note: After completion, this will create a components.json
file located in the root directory of your project and a lib
folder at /src/lib
.
As you can see, shadcn does not automatically install any primitive components. This gives you complete freedom to choose and customize the components you want to use in your project. Unlike other libraries that enforce a rigid structure or automatically include default components, shadcn allows you to have full control over the elements that make up your user interface.
Using shadcn/ui Components
Now that we have set up the basics, it’s time to clean up the boilerplate code generated by Next.js. While the default setup provides a good starting point, it includes unnecessary files and code that aren’t relevant to our goal of focusing on ShadCN components.
You should have something like that in your code:
export default function Home() {
return (
<div>
<h1>Shadcn with Next.js 15</h1>
</div>
);
}

Now we are ready to add some shadcn/ui components.
How to add a shadcn/ui component
Pick a component at shadcn/ui docs.
- In shadcn/ui documentation we can search for the component we want to add at our project, for this tutorial I will add the button component, you can check it here.
- If this is the first component you are installing, a new directory named
/components/ui
will be created inside the/src
folder. This/ui
directory will contain all the ShadCN UI components you add to your project.
Copy the CLI command.
- To add it with
pnpm
use:
pnpm dlx shadcn@latest add button
Note: you can also select other package manager as
npm
,yarn
orbun
.- To add it with
Import the component inside any page you want to use it.
Styling comparison between shadcn and vanilla tailwind
export default function Home() {
return (
<div>
<h1 className="text-white">Shadcn with Next.js 15</h1>
<button className="bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 h-9 px-4 py-2 inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm">button</button>
</div>
);
}
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div>
<h1 className="text-white">Shadcn with Next.js 15</h1>
<Button variant={"secondary"}>button</Button>
</div>
);
}
Same result:
As you can see, both approaches yield the same visual result, but using shadcn makes the process far more efficient and scalable. Instead of manually applying utility classes for every button, shadcn allows you to leverage pre-built, customizable components, significantly reducing repetitive code and improving maintainability.
Let's take a look at the button.tsx file to understand the additional functionality shadcn provides:
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
The button.tsx
file does much of the heavy lifting for us:
- Predefined Variants: It includes style variants (default, secondary, outline, etc.) and size options (sm, lg, icon, etc.).
- Accessibility Features: Adds accessibility attributes like focus-visible and handles disabled states.
- Customization Options: Using cva (Class Variance Authority), you can easily extend or modify button styles as needed.
How to customizing shadcn/ui components
One of the key reasons to use shadcn is the flexibility it offers in customizing components. Since you are adding the components directly to your project, you have full access to their source files. This means you can modify them whenever necessary. You can easily remove, add, or adjust variants and pre-configured styles to align with your project's design requirements, giving you complete control over the appearance and functionality of your components. For example I could do the following changes:
- remove the
secondary
variant; - add a new variant named
my-variant
; - add a new size named 'big-btn'.
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
'my-variant': "bg-green-600 hover:bg-green-600/70 text-white font-bold"
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
'big-btn': 'h-20 px-6 py-4',
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
You can easily manage the new variants by setting them as defaultVariants
(this will be applied in each component that doesn't have a selected variant), or you can select them directly in the file where you imported the shadcn button. This flexibility allows you to customize the button's appearance and behavior without any hassle.
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div>
<h1 className="text-white">Shadcn with Next.js 15</h1>
<Button variant={"my-variant"} size={"big-btn"}>button</Button>
</div>
);
}
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
'my-variant': "bg-green-600 hover:bg-green-600/70 text-white font-bold"
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
'big-btn': 'h-20 w-20',
},
},
defaultVariants: {
variant: "my-variant",
size: "big-btn",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
The result will be the same:
Here you can check the differences between some of the variants:
Conclusion
Integrating shadcn/ui with Next.js 15 streamlines the process of building modern, scalable UIs. With its accessible, customizable components and seamless integration with Tailwind CSS, shadcn/ui simplifies development while giving full control over style and functionality.
By following this guide, you’ve learned how to quickly set up and customize components in your Next.js project. The flexibility and ease of use make shadcn/ui an excellent choice for developers seeking to build efficient, maintainable applications.