# How to Use shadcn/ui and Radix UI Primitives in Next.js: A Complete Implementation Guide

> Learn to implement shadcn/ui and Radix UI primitives in Next.js 14. Discover how to build a type-safe design system using Tailwind CSS and a utility-based pattern with this complete guide.

- Repository: [Ege Chelebi/blog](https://github.com/woosal1337/blog)
- Tags: how-to-guide
- Published: 2026-08-06

---

**The woosal1337/blog repository demonstrates how to build a lightweight, type-safe design system by wrapping Radix UI primitives with Tailwind CSS classes and a utility-based pattern in Next.js 14.**

This article explores the practical implementation of shadcn/ui components within a Next.js 14 application using the App Router. By examining the `woosal1337/blog` codebase, you will learn how to combine Radix UI primitives with Tailwind CSS to create extensible, accessible UI components that work seamlessly across server and client boundaries.

## Core Architecture of the Design System

The design system follows the shadcn/ui philosophy: unopinionated, composable components that you own and can customize. Unlike traditional component libraries, these primitives live directly in your source code.

### The `cn` Utility for Class Management

Central to the architecture is the `cn` helper function defined in [[`lib/utils.tsx`](https://github.com/woosal1337/blog/blob/main/lib/utils.tsx)](https://github.com/woosal1337/blog/blob/main/lib/utils.tsx). This utility combines **`class-variance-authority`** (CVA) for variant management with **`tailwind-merge`** to resolve conflicting Tailwind classes safely.

```tsx
// lib/utils.tsx
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

```

Every UI component uses this function to merge base styles, variant classes, and user-provided overrides without class collisions.

### Component Structure

Components reside in `components/ui/` and follow a consistent pattern:

1. Import the Radix primitive (e.g., `@radix-ui/react-dialog`)
2. Define TypeScript interfaces extending the primitive's props
3. Apply Tailwind classes using the `cn` utility
4. Export the component with forwardRef support

This approach ensures full type safety while maintaining the flexibility to override any style or behavior.

## Implementing the Button Component

The **Button** component in [[`components/ui/button.tsx`](https://github.com/woosal1337/blog/blob/main/components/ui/button.tsx)](https://github.com/woosal1337/blog/blob/main/components/ui/button.tsx) leverages **`@radix-ui/react-slot`** to allow polymorphic rendering. This means you can render the button as any element—an anchor tag, a Next.js `Link`, or a native button—while preserving all styling and behavior.

```tsx
import { Button } from "@/components/ui/button";
import Link from "next/link";

export default function Navigation() {
  return (
    <Button asChild variant="outline" size="lg">
      <Link href="/projects">View Projects</Link>
    </Button>
  );
}

```

The component uses **CVA** (class-variance-authority) to define style variants:

- **Variants**: `default`, `destructive`, `outline`, `secondary`, `ghost`, `link`
- **Sizes**: `default`, `sm`, `lg`, `icon`

Each variant combination resolves to specific Tailwind classes for colors, borders, and hover states, all merged safely via the `cn` utility.

## Building Accessible Dialogs

The **Dialog** implementation in [[`components/ui/dialog.tsx`](https://github.com/woosal1337/blog/blob/main/components/ui/dialog.tsx)](https://github.com/woosal1337/blog/blob/main/components/ui/dialog.tsx) wraps **`@radix-ui/react-dialog`** to provide a fully accessible modal system with keyboard navigation and focus management.

The component exports several composable parts:

- `Dialog` - The root provider
- `DialogTrigger` - The element that opens the dialog
- `DialogContent` - The modal container with overlay
- `DialogHeader`, `DialogFooter` - Layout containers
- `DialogTitle`, `DialogDescription` - Semantic heading elements

```tsx
"use client";

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export default function ProjectModal() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Open Details</Button>
      </DialogTrigger>
      
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Project Information</DialogTitle>
          <DialogDescription>
            Built with Next.js 14, shadcn/ui, and Radix primitives.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  );
}

```

Notice the `"use client"` directive. Dialog components require browser APIs for focus trapping and portal rendering, making them client components. The styling uses Tailwind's data attribute selectors like `data-[state=open]:animate-in` to handle enter/exit animations based on Radix's state machine.

## Server and Client Component Boundaries

In the Next.js 14 App Router architecture used in this repository, component placement determines render behavior:

- **Server Components**: Static UI elements that don't require interaction can remain server-rendered for optimal performance
- **Client Components**: Interactive primitives like `Dialog`, `DropdownMenu`, or `Carousel` must include the `"use client"` directive at the top of the file

The **Slot** pattern from Radix is particularly valuable here. It allows you to pass a server-rendered `Link` component as a child to a client-rendered `Button`, maintaining SEO benefits while enabling client-side navigation.

## Adding Custom Components

To extend the design system with new shadcn/ui-style components:

1. **Install the Radix primitive**: `npm install @radix-ui/react-popover`
2. **Create the wrapper**: Add [`components/ui/popover.tsx`](https://github.com/woosal1337/blog/blob/main/components/ui/popover.tsx)
3. **Apply the pattern**: Import Radix parts, wrap with Tailwind classes using `cn`, and export with `forwardRef`
4. **Handle client/server**: Add `"use client"` if the component uses browser APIs

All components should reference the project's design tokens (e.g., `bg-background`, `text-muted-foreground`, `border-border`) defined in your Tailwind configuration to maintain consistency with the dark-only theme.

## Summary

- **The `cn` utility** in [`lib/utils.tsx`](https://github.com/woosal1337/blog/blob/main/lib/utils.tsx) combines `tailwind-merge` and `clsx` to handle conditional class merging safely across components.
- **Radix primitives** provide unstyled, accessible behavior for complex interactions like dialogs and slots, which you style with Tailwind in `components/ui/`.
- **The Slot pattern** enables polymorphic components, allowing a Button to render as a Link while preserving type safety.
- **Server/Client boundaries** require the `"use client"` directive for interactive components that use browser APIs, but static wrappers can remain server-rendered.
- **CVA** manages component variants consistently, making it easy to extend styles while preventing class conflicts.

## Frequently Asked Questions

### What is the difference between shadcn/ui and traditional UI libraries?

Unlike traditional libraries where you install a package from npm, shadcn/ui provides code patterns that you copy directly into your project. You own the components in `components/ui/`, meaning you can customize every Tailwind class and behavior without fighting against a library's API constraints. The `woosal1337/blog` repository demonstrates this by maintaining full control over the Button and Dialog implementations while relying on Radix for complex accessibility logic.

### Why does the Button component use `@radix-ui/react-slot`?

The Slot primitive allows the Button to accept an `asChild` prop that merges the Button's props and styling onto its immediate child element. This enables you to render the Button as a Next.js `Link`, an anchor tag, or any other element while maintaining consistent styling and behavior. Without Slot, you would need separate components for `Button` and `ButtonLink`, duplicating logic.

### When should I use "use client" in shadcn/ui components?

Add the `"use client"` directive at the top of any component file that uses browser-only APIs, React hooks like `useState` or `useEffect`, or Radix primitives that require DOM access. In the `woosal1337/blog` repository, components like Dialog require this directive for focus management and portal rendering, while simple layout components can remain server-rendered for better performance.

### How do I customize the color variants for shadcn/ui components?

Modify the CVA (class-variance-authority) configuration within each component file. For example, in [`components/ui/button.tsx`](https://github.com/woosal1337/blog/blob/main/components/ui/button.tsx), locate the `buttonVariants` object and add or modify entries in the `variants` object. Use the `cn` utility when applying classes to ensure your custom colors merge correctly with base styles and any Tailwind classes passed via the `className` prop.