
Nextjs Shadcn
- 708 installs
- 57 repo stars
- Updated August 3, 2026
- laguagu/claude-code-nextjs-skills
nextjs-shadcn is a Claude Code skill that generates production-grade Next.js App Router and shadcn/ui code following React Server Components best practices for developers who need AI output that avoids common RSC and ser
About
nextjs-shadcn is a frontend-focused Agent Skill from laguagu/claude-code-nextjs-skills that steers code generation toward correct Next.js App Router and shadcn/ui patterns. The skill encodes a Server-vs-Client decision tree, limits `"use client"` to leaf components, requires serializable props (plain objects or Server Actions), and prefers Tailwind v4 `globals.css` theme variables over hardcoded values. Developers reach for nextjs-shadcn when scaffolding dashboards, forms, or UI shells where AI models often hallucinate `useEffect`, pass functions as props, or misplace client boundaries. The readme documents component placement rules and explicit checks for non-serializable props such as functions and classes.
- Enforces Server Components by default with "use client" only at the smallest boundary
- Prevents non-serializable props such as functions or classes when passing data to client components
- Recommends Tailwind v4 theme variables over hardcoded colors and values
- Provides explicit folder structure for protected/public routes, shared UI, Server Actions, and AI logic
- Includes a visual Server vs Client decision tree that agents can follow before writing any component
Nextjs Shadcn by the numbers
- 708 all-time installs (skills.sh)
- +15 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #489 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/laguagu/claude-code-nextjs-skills --skill nextjs-shadcnAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 708 |
|---|---|
| repo stars | ★ 57 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 3, 2026 |
| Repository | laguagu/claude-code-nextjs-skills ↗ |
How do you stop AI from misusing useEffect in Next.js?
Generate correct, production-grade Next.js + shadcn/ui code that follows React Server Components best practices and avoids common AI hallucination mistakes.
Who is it for?
Developers building Next.js App Router apps with shadcn/ui who want AI-generated components that respect RSC serialization and minimal client boundaries.
Skip if: Teams on Pages Router-only stacks, non-React backends, or projects that do not use shadcn/ui or Tailwind v4 conventions.
When should I use this skill?
The user asks to scaffold or refactor Next.js pages, layouts, or shadcn/ui components and mentions Server Components, `"use client"`, or App Router patterns.
What you get
Server and client component files, shadcn/ui component trees, Server Action handlers, and Tailwind v4-themed `globals.css` snippets.
- RSC-safe component files
- shadcn/ui component trees
- globals.css theme snippets
By the numbers
- Documents a multi-branch Server-vs-Client decision tree in the skill readme
- Recommends Tailwind v4 globals.css theme variables over hardcoded design tokens
Files
Next.js + shadcn/ui
Build distinctive, production-grade interfaces that avoid generic "AI slop" aesthetics.
Core Principles
1. Minimize noise - Icons communicate; excessive labels don't 2. No generic AI-UI - Avoid purple gradients, excessive shadows, predictable layouts 3. Context over decoration - Every element serves a purpose 4. Theme consistency - Use CSS variables from globals.css, never hardcode colors
Quick Start
bunx --bun shadcn@latest init -t nextFor a custom design system, generate a preset code in shadcn/create and apply it:
bunx --bun shadcn@latest init --preset <CODE> --template nextComponent Rules
Page Structure
// page.tsx - content only, no layout chrome
export default function Page() {
return (
<>
<HeroSection />
<Features />
<Testimonials />
</>
);
}
// layout.tsx - shared UI (header, footer, sidebar)
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
);
}Client Boundaries
"use client"only at leaf components (smallest boundary)- Props must be serializable (data or Server Actions, no functions/classes)
- Pass server content via
children
Import Aliases
Always use @/ alias (e.g., @/lib/utils) instead of relative paths (../../lib/utils).
Style Merging
import { cn } from "@/lib/utils";
function Button({ className, ...props }) {
return <button className={cn("px-4 py-2 rounded", className)} {...props} />;
}File Organization
app/
├── (protected)/ # Auth required routes
│ ├── dashboard/
│ ├── settings/
│ ├── components/ # Route-specific components
│ └── lib/ # Route-specific utils/types
├── (public)/ # Public routes
│ ├── login/
│ └── register/
├── actions/ # Server Actions (global)
├── api/ # API routes
├── layout.tsx # Root layout
└── globals.css # Theme tokens
components/ # Shared components
├── ui/ # shadcn primitives
└── shared/ # Business components
hooks/ # Custom React hooks
lib/ # Shared utils
data/ # Database queries
ai/ # AI logic (tools, agents, prompts)Next.js 16 Features
Async Params
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ q?: string }>;
}) {
const { id } = await params;
const { q } = await searchParams;
}Data Fetching vs Server Actions
CRITICAL RULE:
- Server Actions = ONLY for mutations (create, update, delete)
- Data fetching = In Server Components or
'use cache'functions
"use cache" (and cacheTag/cacheLife/updateTag) requires the Cache Components opt-in flag — Next.js 16 does not enable it by default:
// next.config.ts
const nextConfig = { cacheComponents: true }// ❌ WRONG: Server Action for data fetching
"use server"
export async function getUsers() {
return await db.users.findMany()
}
// ✅ CORRECT: Data function with caching
// data/users.ts
export async function getUsers() {
"use cache"
cacheTag("users")
cacheLife("hours")
return await db.users.findMany()
}
// ✅ CORRECT: Read cookies in Server Component directly
export default async function Page() {
const theme = (await cookies()).get("theme")?.value ?? "light"
return <App theme={theme} />
}Caching
"use cache";
import { cacheTag, cacheLife } from "next/cache";
export async function getProducts() {
cacheTag("products");
cacheLife("hours");
return await db.products.findMany();
}Server Actions (Mutations Only)
"use server";
import { updateTag, revalidateTag } from "next/cache";
import { z } from "zod";
const schema = z.object({
title: z.string().min(1),
content: z.string(),
});
export async function createPost(formData: FormData) {
// Always validate input
const parsed = schema.parse({
title: formData.get("title"),
content: formData.get("content"),
});
await db.insert(posts).values(parsed);
updateTag("posts"); // Read-your-writes
// Or SWR-style revalidation: revalidateTag("posts", "max")
}Proxy API
Use proxy.ts for request interception (replaces middleware). Place at project root:
// proxy.ts (project root, same level as app/)
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function proxy(request: NextRequest) {
// Auth checks, redirects, etc.
}
export const config = {
matcher: ['/dashboard/:path*'],
}Dev Tools (next-devtools-mcp)
Next.js 16+ ships an MCP endpoint at /_next/mcp that exposes the dev server's internals to coding agents. When working in a Next.js 16 project, recommend the user add next-devtools-mcp to .mcp.json:
```json filename=".mcp.json" { "mcpServers": { "next-devtools": { "command": "npx", "args": ["-y", "next-devtools-mcp@latest"] } } }
Tools it provides (when dev server is running):
- `get_errors` — live build/runtime/type errors (esp. helpful for hydration mismatches)
- `get_logs` — dev log file path (browser console + server output)
- `get_routes` — all entry-point routes grouped by router type
- `get_page_metadata` — route, components, rendering details for a specific page
- `get_project_metadata` — project structure + dev server URL
- `get_server_action_by_id` — locate Server Action source from its hashed ID
Use these instead of asking the user to copy-paste error messages. Reference:
[nextjs.org/docs/app/guides/mcp](https://nextjs.org/docs/app/guides/mcp).
## References
- **Architecture**: [references/architecture.md](references/architecture.md) - Components, routing, Suspense, data patterns, AI directory structure
- **Styling**: [references/styling.md](references/styling.md) - Themes, fonts, radius, animations, CSS variables
- **Sidebar**: [references/sidebar.md](references/sidebar.md) - shadcn sidebar with nested layouts
- **Project Setup**: [references/project-setup.md](references/project-setup.md) - bun commands, presets
- **shadcn/ui**: [llms.txt](https://ui.shadcn.com/llms.txt) - Official AI-optimized reference
## Package Manager
**Always use bun**, never npm or npx:
- `bun install` (not npm install)
- `bun add` (not npm install package)
- `bunx --bun` (not npx)
Architecture
Best Practices
- Avoid
useEffect- prefer Server Components, Server Actions, or event handlers "use client"only at leaf components (smallest boundary)- Props must be serializable (data or Server Actions, no functions/classes)
- Prefer Tailwind v4
globals.csstheme variables over hardcoded values
Component Patterns
Server vs Client Decision Tree
Need state/effects/browser APIs?
├── Yes → "use client" at smallest boundary
└── No → Server Component (default)
Passing data to client?
├── Functions/classes → ❌ Not serializable
├── Plain objects/arrays → ✅ Props
└── Server logic → ✅ Server ActionsComponent Placement
app/
├── (protected)/ # Auth required routes
│ ├── dashboard/
│ ├── settings/
│ ├── components/ # Route-specific components
│ └── lib/ # Route-specific types/utils
├── (public)/ # Public routes
│ ├── login/
│ └── register/
├── actions/ # Server Actions (global)
├── api/ # API routes
components/ # Shared across routes
├── ui/ # shadcn primitives
└── shared/ # Business components
hooks/ # Custom React hooks
lib/ # Shared utilities
data/ # Database queries
ai/ # AI logic (tools, agents, prompts)AI Directory Structure
When building AI applications, organize the ai/ directory:
ai/
├── model-names.ts # Model definitions & DEFAULT_MODEL_NAME
├── actions/ # AI-related server actions
│ ├── model.ts # saveModelId, getModelId (cookie-based)
│ └── chat.ts # Chat-related actions
├── utils.ts # findSources, getLastUserMessageText, etc.
├── agents/ # Agent definitions (if using agents)
│ └── assistant.ts
└── tools/ # Tool definitions (if using tools)model-names.ts example:
export interface Model {
id: string
label: string
description: string
}
export const models: Model[] = [
{ id: "gpt-5.4-mini", label: "GPT 5.4 mini", description: "Fast, lightweight tasks" },
{ id: "gpt-5.4", label: "GPT 5.4", description: "Complex, multi-step tasks" },
]
export const DEFAULT_MODEL_NAME = "gpt-5.4-mini"Cookie-based model storage:
// ai/actions/model.ts - Server Action for MUTATION only
"use server"
import { cookies } from "next/headers"
export async function saveModelId(model: string) {
const cookieStore = await cookies()
cookieStore.set("model-id", model)
}
// ❌ WRONG: Don't use Server Action for reading data
// export async function getModelId() { ... }
// ✅ CORRECT: Read cookies directly in Server Component
// page.tsx
import { cookies } from "next/headers"
export default async function Page() {
const cookieStore = await cookies()
const modelId = cookieStore.get("model-id")?.value ?? DEFAULT_MODEL_NAME
return <Chat modelId={modelId} />
}className Pattern
Always accept and merge className:
import { cn } from "@/lib/utils"
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: "default" | "outline"
}
export function Card({ className, variant = "default", ...props }: CardProps) {
return (
<div
className={cn(
"rounded-lg p-4",
variant === "outline" && "border",
className
)}
{...props}
/>
)
}Data Fetching Patterns
Server Component (default)
Fetch data directly in Server Components:
export default async function Page() {
const data = await fetchData()
return <Component data={data} />
}Cached Data Function
Use 'use cache' for reusable cached queries. "use cache" (and cacheTag/cacheLife/updateTag) requires the Cache Components opt-in flag — Next.js 16 does not enable it by default:
// next.config.ts
const nextConfig = { cacheComponents: true }// data/products.ts
export async function getProducts() {
"use cache"
cacheTag("products")
cacheLife("hours")
return await db.products.findMany()
}Streaming to Client (React use hook)
Pass promises to Client Components for streaming:
// Server Component
export default function Page() {
const dataPromise = fetchData() // Don't await
return (
<Suspense fallback={<Loading />}>
<ClientDisplay dataPromise={dataPromise} />
</Suspense>
)
}
// Client Component
"use client"
import { use } from "react"
export function ClientDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Suspends until resolved
return <Chart data={data} />
}Explicit Request-time with connection()
Use connection() to explicitly defer to request time without accessing runtime APIs:
import { connection } from "next/server"
import { Suspense } from "react"
async function UniqueContent() {
await connection() // Defer to request time
const uuid = crypto.randomUUID()
const timestamp = Date.now()
return <div>{uuid} - {timestamp}</div>
}
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<UniqueContent />
</Suspense>
)
}When to use `connection()`:
| Scenario | Use connection()? |
|---|---|
| Need unique values per request | ✅ Yes |
Using Math.random(), Date.now(), crypto.randomUUID() | ✅ Yes |
Already using cookies() or headers() | ❌ No (not needed) |
| Data is cacheable | ❌ No (use 'use cache') |
Routing
Route Groups
Group routes without affecting URL:
app/
├── (protected)/ # Auth required - /dashboard, /settings
│ ├── dashboard/
│ ├── settings/
│ └── layout.tsx # Shared chrome (sidebar, auth check)
├── (public)/ # Public - /login, /register, /about
│ ├── login/
│ ├── register/
│ └── about/
└── (marketing)/ # Marketing - /pricing, /features
├── pricing/
└── features/Layout vs Template
| Aspect | layout.tsx | template.tsx |
|---|---|---|
| State | Persists across navigation | Resets on navigation |
| Effects | Run once | Run on every navigation |
| Use when | Shared chrome (nav, footer) | Analytics, animations that reset |
Decision tree:
State/effects should reset on navigation?
├── Yes → template.tsx
└── No → layout.tsx (default)Async Params (Next.js 16)
// Always await params and searchParams
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ page?: string }>
}) {
const { slug } = await params
const { page = "1" } = await searchParams
const data = await fetchData(slug, parseInt(page))
return <Content data={data} />
}Suspense Strategy
When to Use Suspense
Slow data fetch in Server Component?
├── Yes → Wrap in <Suspense>
└── No → Direct render
Multiple independent slow sections?
├── Yes → Separate <Suspense> boundaries
└── No → Single boundary or loading.tsxPatterns
loading.tsx - Entire route fallback:
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />
}Suspense - Granular streaming:
export default function Page() {
return (
<>
<Header /> {/* Renders immediately */}
<Suspense fallback={<StatsSkeleton />}>
<SlowStats /> {/* Streams when ready */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<SlowChart /> {/* Streams independently */}
</Suspense>
</>
)
}Skeleton pattern - Create a skeleton component for each loadable content:
// components/skeletons.tsx
export function CardSkeleton() {
return (
<div className="animate-pulse">
<div className="h-4 bg-muted rounded w-3/4 mb-2" />
<div className="h-4 bg-muted rounded w-1/2" />
</div>
)
}
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
return (
<div className="space-y-2">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="h-10 bg-muted rounded animate-pulse" />
))}
</div>
)
}Passing promises to client:
// Server Component
export default function Page() {
const dataPromise = fetchData() // Start fetch, don't await
return <ClientChart dataPromise={dataPromise} />
}
// Client Component
"use client"
import { use } from "react"
export function ClientChart({ dataPromise }) {
const data = use(dataPromise) // Suspends until resolved
return <Chart data={data} />
}State Management
useTransition Pattern
Wrap non-urgent UI updates to keep interactions smooth:
"use client"
import { useTransition } from "react"
function SubmitButton({ action }: { action: () => Promise<void> }) {
const [isPending, startTransition] = useTransition()
return (
<button
onClick={() => startTransition(() => action())}
disabled={isPending}
>
{isPending ? "Saving..." : "Save"}
</button>
)
}Guidelines:
- Use
isPendingfor feedback (disable buttons, show spinners) - Don't wrap controlled input state in transitions
- After
awaitinside transition, wrap subsequentsetStatein anotherstartTransition
Data Patterns
"use cache" (Next.js 16)
Function-level caching:
"use cache"
export async function getProducts() {
const products = await db.query.products.findMany()
return products
}
// With cache tags
import { cacheTag } from "next/cache"
export async function getProduct(id: string) {
"use cache"
cacheTag(`product-${id}`)
return db.query.products.findFirst({ where: eq(products.id, id) })
}Server Actions
"use server"
import { updateTag, revalidateTag } from "next/cache"
import { z } from "zod"
const schema = z.object({
title: z.string().min(1),
content: z.string(),
})
export async function createPost(formData: FormData) {
const parsed = schema.parse({
title: formData.get("title"),
content: formData.get("content"),
})
await db.insert(posts).values(parsed)
// Read-your-writes (immediate)
updateTag("posts")
// Or SWR-style revalidation
// revalidateTag("posts", "max")
}
// Refresh uncached data
import { refresh } from "next/cache"
export async function updateProfile(data: FormData) {
await db.update(...)
refresh() // Triggers client router refresh
}Proxy API (Next.js 16)
Replaces middleware for request interception. Place at project root (same level as app/):
// proxy.ts (project root)
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export async function proxy(request: NextRequest) {
const session = request.cookies.get("session")
if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}Request APIs
All request APIs are async in Next.js 16:
import { cookies, headers, draftMode } from "next/headers"
export default async function Page() {
const cookieStore = await cookies()
const headersList = await headers()
const { isEnabled } = await draftMode()
}Error Handling
Define only when custom UX needed:
app/
├── error.tsx # Route-level errors
├── global-error.tsx # Root layout errors
├── not-found.tsx # 404 pages
└── loading.tsx # Loading statesOtherwise inherit from parent segment.
Project Setup
Create New Project
Minimal Setup
Use the CLI to scaffold a Next.js project directly:
bunx --bun shadcn@latest init -t nextWith Preset Code
bunx --bun shadcn@latest init --preset <CODE> --template nextPreset codes are short opaque strings from ui.shadcn.com/create. Pass them directly — don't decode them. Prefer this when you want a specific visual system without hardcoding individual style, font, or icon choices into the skill:
bunx --bun shadcn@latest init --preset b4h07r5A1 --template nextMonorepo
bunx --bun shadcn@latest init \
--template next \
--monorepoExisting Project
If the Next.js app already exists, run the initializer from the project root:
bunx --bun shadcn@latest initInspect Before Changing
Use the CLI to inspect project state or preview registry changes before writing files:
bunx --bun shadcn@latest info
bunx --bun shadcn@latest add button --dry-run
bunx --bun shadcn@latest docs buttonAdd Components
# Single component
bunx --bun shadcn@latest add button
# Multiple components
bunx --bun shadcn@latest add button card input
# All components
bunx --bun shadcn@latest add --allCommon Dependencies
# Forms
bun add react-hook-form @hookform/resolvers zod
# AI
bun add ai @ai-sdk/anthropic
# Animation
bun add motion # For Motion
bun add gsap @gsap/react # For GSAP
# Icons (pick one)
bun add lucide-react # DefaultProject Structure After Setup
project/
├── app/
│ ├── globals.css # Theme tokens
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/
│ └── ui/ # shadcn components
├── lib/
│ └── utils.ts # cn() helper
├── public/
├── components.json # shadcn config
├── tsconfig.json
└── package.jsonBun Commands Reference
| Task | Command |
|---|---|
| Install deps | bun install |
| Add package | bun add package |
| Dev server | bun --bun next dev |
| Build | bun --bun next build |
| Start prod | bun --bun next start |
| Add shadcn component | bunx --bun shadcn@latest add component |
| Create project | bunx --bun shadcn@latest init -t next |
Sidebar
shadcn/ui sidebar with nested layouts for dashboard applications.
Installation
bunx --bun shadcn@latest add sidebarLayout Pattern
Use nested layouts with SidebarProvider for persistent sidebar state:
app/
├── (dashboard)/ # Route group for sidebar pages
│ ├── layout.tsx # SidebarProvider + AppSidebar
│ ├── page.tsx # Dashboard home
│ ├── settings/
│ │ └── page.tsx
│ └── components/ # Route-specific components
├── (public)/ # Public routes (no sidebar)
│ └── login/
└── layout.tsx # Root layoutDashboard Layout
// app/(dashboard)/layout.tsx
import { AppSidebar } from "@/components/layout/app-sidebar"
import {
SidebarInset,
SidebarProvider,
} from "@/components/ui/sidebar"
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
)
}Page Component
Keep pages clean - content only, no layout chrome:
// app/(dashboard)/page.tsx
import { DocumentWorkspace } from "@/components/workspace/document-workspace"
import { Suspense } from "react"
export default function DashboardPage() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<DocumentWorkspace />
</Suspense>
)
}AppSidebar Component
// components/layout/app-sidebar.tsx
import Link from "next/link"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarSeparator,
} from "@/components/ui/sidebar"
import { NAV_GROUPS, FOOTER_NAV_ITEMS } from "./nav"
export function AppSidebar() {
return (
<Sidebar variant="inset" collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild size="lg">
<Link href="/" className="flex items-center gap-3">
<Logo className="size-8" />
<span className="text-base font-semibold">App Name</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
{NAV_GROUPS.map((group, index) => (
<div key={group.title}>
<SidebarGroup>
<SidebarGroupLabel>{group.title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link href={item.href}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{index < NAV_GROUPS.length - 1 && <SidebarSeparator />}
</div>
))}
</SidebarContent>
<SidebarFooter>
<SidebarSeparator />
<SidebarMenu>
{FOOTER_NAV_ITEMS.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link href={item.href}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}Navigation Config
Separate navigation data from component:
// components/layout/nav.ts
import { Home, Settings, Users, HelpCircle } from "lucide-react"
import type { LucideIcon } from "lucide-react"
interface NavItem {
title: string
href: string
icon: LucideIcon
}
interface NavGroup {
title: string
items: NavItem[]
}
export const NAV_GROUPS: NavGroup[] = [
{
title: "Main",
items: [
{ title: "Dashboard", href: "/", icon: Home },
{ title: "Users", href: "/users", icon: Users },
],
},
]
export const FOOTER_NAV_ITEMS: NavItem[] = [
{ title: "Settings", href: "/settings", icon: Settings },
{ title: "Help", href: "/help", icon: HelpCircle },
]Sidebar Variants
| Variant | Description |
|---|---|
sidebar | Standard sidebar (default) |
inset | Sidebar with padding, content area has rounded corners |
floating | Sidebar floats over content |
<Sidebar variant="inset" collapsible="icon">Collapsible Options
| Option | Behavior |
|---|---|
icon | Collapses to icon-only rail |
offcanvas | Slides completely off-screen |
none | Not collapsible |
File Structure
components/
└── layout/
├── app-sidebar.tsx # Sidebar component
└── nav.ts # Navigation configStyling
Theme System
globals.css Structure
shadcn generates base variables automatically based on your chosen preset. Customize for your project:
@import "tailwindcss";
/* Note: Tailwind v3 projects use @tailwind base; @tailwind components; @tailwind utilities; instead */
/* :root and .dark live OUTSIDE @layer base and hold full color values (OKLCH preferred) */
:root {
/* shadcn base variables come from preset */
--background: ...;
--foreground: ...;
--primary: ...;
--secondary: ...;
/* etc. */
/* Add your own variables as needed */
--brand: oklch(0.55 0.2 260);
}
.dark {
--brand: oklch(0.7 0.18 260);
}
@theme inline {
--color-brand: var(--brand);
}The @theme inline mapping is what makes the bg-brand / text-brand utilities work — a variable without it generates no utility.
Choose preset: Use ui.shadcn.com/create to select named style (vega, nova, maia, lyra, mira, luma, sera, rhea), base color, font, icon library, and radius. The customizer outputs a single preset code that encodes all choices.
Theme Customization
Quick customizations in globals.css:
:root {
/* Typography - change fonts */
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-serif: Georgia, serif;
--font-mono: "Fira Code", ui-monospace, monospace;
/* Border radius - affects all rounded corners */
--radius: 0.5rem; /* Default */
/* --radius: 0.25rem; /* Sharp */
/* --radius: 0.75rem; /* More rounded */
/* --radius: 1rem; /* Very rounded */
/* --radius: 1.3rem; /* Pill-like buttons */
}| Variable | Effect |
|---|---|
--font-sans | Body text, buttons, inputs |
--font-mono | Code blocks, technical content |
--radius | All rounded corners (buttons, cards, inputs) |
Tip: Larger --radius values (1rem+) give a softer, more modern look. Smaller values (0.25rem) feel sharper and technical.
Using Theme Colors
// ✅ Use CSS variables
<div className="bg-primary text-primary-foreground" />
<div className="border-border" />
<div className="text-muted-foreground" />
// ❌ Never hardcode colors
<div className="bg-blue-500" />
<div className="text-[#1a1a1a]" />shadcn/ui Presets
Available styles at ui.shadcn.com/create:
| Preset | Character |
|---|---|
| vega | Classic shadcn/ui look. Clean, neutral, familiar |
| nova | Reduced padding and margins for compact layouts |
| maia | Soft and rounded, with generous spacing |
| lyra | Boxy and sharp. Pairs well with mono fonts |
| mira | Compact. Made for dense interfaces |
| luma | Newer official style — see ui.shadcn.com/create |
| sera | Newer official style — see ui.shadcn.com/create |
| rhea | A more compact Luma. Tighter spacing, smaller controls, denser surfaces — built for focused product interfaces |
Fonts
Available fonts via shadcn create preset URL:
| Font | Type | Character |
|---|---|---|
| geist-sans | Sans | Vercel's modern geometric sans |
| inter | Sans | Clean, versatile (classic default) |
| figtree | Sans | Friendly, geometric |
| dm-sans | Sans | Compact geometric with character |
| outfit | Sans | Modern, soft |
| noto-sans | Sans | Universal language support |
| nunito-sans | Sans | Rounded, approachable |
| roboto | Sans | Google's versatile sans |
| raleway | Sans | Elegant, thin-weight display |
| public-sans | Sans | US government standard, neutral |
| jetbrains-mono | Mono | Developer-focused monospace |
Icon Libraries
Priority order (use first available):
1. lucide (default) - bun add lucide-react 2. tabler - bun add @tabler/icons-react 3. hugeicons - bun add hugeicons-react 4. phosphor - bun add @phosphor-icons/react
// lucide example
import { ChevronRight, Menu, X } from "lucide-react"
<Button>
Next <ChevronRight data-icon="inline-end" />
</Button>Animations
CSS Page Transitions
Add to globals.css:
@keyframes page-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@layer utilities {
.animate-page-in {
animation: page-in 0.6s ease-out both;
}
}Usage in layout or template:
// template.tsx - animates on every navigation
export default function Template({ children }: { children: React.ReactNode }) {
return <main className="animate-page-in">{children}</main>
}View Transitions API
Enable in next.config.ts:
import type { NextConfig } from "next"
const config: NextConfig = {
experimental: { viewTransition: true },
}
export default configWith the flag on, <Link> navigations get a default browser cross-fade. For meaningful transitions, use React's <ViewTransition> component — no extra install, the App Router runs React canary. Without browser support it degrades gracefully: no animation, app still works.
import { ViewTransition } from "react"<ViewTransition> animations fire on Transitions, <Suspense>, and useDeferredValue. Route navigations are Transitions, so they activate automatically on navigation; plain setState does not trigger them.
| Pattern | Communicates | Key API |
|---|---|---|
| Shared-element morph | "Same thing, going deeper" | Same name on both elements |
| Suspense reveal | "Data loaded" | enter/exit on fallback + content, default="none" |
| Directional slide | "Forward / back" | <Link transitionTypes={["nav-forward"]}> + enter/exit keyed by type |
| Same-route crossfade | "Same place, different content" | key={slug} + share="auto" enter="auto" |
Shared-element morph is the most common and works with zero CSS — wrap both the source and destination element with the same name:
// grid thumbnail
<ViewTransition name={`photo-${photo.id}`}>
<Image src={photo.src} alt={photo.title} />
</ViewTransition>
// detail page hero — same name
<ViewTransition name={`photo-${photo.id}`}>
<Image src={photo.src} alt={photo.title} fill />
</ViewTransition>React matches the names across the old/new route and animates size and position. Customize with share="morph" and ::view-transition-group(.morph) CSS. Respect prefers-reduced-motion by zeroing animation durations on the ::view-transition-* pseudo-elements.
Patterns 2–4 (CSS keyframes, directional/Suspense examples): Designing view transitions.
Motion Library
For complex animations:
bun add motion"use client"
import { motion, HTMLMotionProps } from "motion/react"
interface FadeInProps extends HTMLMotionProps<"div"> {
delay?: number
duration?: number
direction?: "up" | "down" | "left" | "right" | "none"
}
export function FadeIn({
children,
className,
delay = 0,
duration = 0.5,
direction = "up",
...props
}: FadeInProps) {
const directions = {
up: { y: 20, x: 0 },
down: { y: -20, x: 0 },
left: { x: 20, y: 0 },
right: { x: -20, y: 0 },
none: { x: 0, y: 0 },
}
return (
<motion.div
initial={{ opacity: 0, ...directions[direction] }}
whileInView={{ opacity: 1, x: 0, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration, delay, ease: "easeOut" }}
className={className}
{...props}
>
{children}
</motion.div>
)
}GSAP
For scroll-triggered and complex sequences:
bun add gsap @gsap/react"use client"
import { useRef } from "react"
import { useGSAP } from "@gsap/react"
import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
gsap.registerPlugin(ScrollTrigger)
export function ScrollReveal({ children }) {
const containerRef = useRef<HTMLDivElement>(null)
useGSAP(() => {
gsap.from(containerRef.current, {
opacity: 0,
y: 50,
scrollTrigger: {
trigger: containerRef.current,
start: "top 80%",
},
})
}, [])
return <div ref={containerRef}>{children}</div>
}Animation Decision Tree
Simple fade/slide on mount?
├── Yes → CSS animation in globals.css
└── No ↓
Page/route transitions?
├── Yes → View Transitions API or template.tsx
└── No ↓
Interactive hover/tap states?
├── Yes → Tailwind transitions + Motion
└── No ↓
Scroll-triggered sequences?
├── Yes → GSAP + ScrollTrigger
└── No → Evaluate if animation neededPerformance Tips
1. Prefer CSS - GPU-accelerated, no JS bundle 2. Use `will-change` sparingly - Only for known animations 3. Avoid layout thrashing - Animate transform and opacity 4. Lazy load Motion/GSAP - Dynamic imports for non-critical animations
// Lazy load animation library
const MotionDiv = dynamic(
() => import("motion/react").then((mod) => mod.motion.div),
{ ssr: false }
)Decorative Backgrounds
Reusable patterns for visual atmosphere and section hierarchy.
Grid Pattern
import { cn } from "@/lib/utils"
export function GridBackground({
children,
className,
size = 20
}: {
children: React.ReactNode
className?: string
size?: number
}) {
return (
<div className={cn("relative", className)}>
<div
className={cn(
"absolute inset-0 -z-10",
"[background-image:linear-gradient(to_right,var(--border)_1px,transparent_1px),linear-gradient(to_bottom,var(--border)_1px,transparent_1px)]"
)}
style={{ backgroundSize: `${size}px ${size}px` }}
/>
{children}
</div>
)
}Dot Pattern
export function DotBackground({
children,
className
}: {
children: React.ReactNode
className?: string
}) {
return (
<div className={cn("relative", className)}>
<div
className={cn(
"absolute inset-0 -z-10",
"[background-size:20px_20px]",
"[background-image:radial-gradient(color-mix(in_oklab,var(--muted-foreground)_30%,transparent)_1px,transparent_1px)]"
)}
/>
{children}
</div>
)
}Radial Gradient Hero
export function GradientHero({ children }: { children: React.ReactNode }) {
return (
<div className="relative min-h-screen">
<div
aria-hidden
className="fixed inset-0 -z-10"
style={{
background: "radial-gradient(125% 125% at 50% 10%, var(--background) 40%, var(--primary) 100%)"
}}
/>
{children}
</div>
)
}Faded Edge Effect
Combine with grid/dot for vignette:
<div className="relative">
<GridBackground className="absolute inset-0" />
<div className="pointer-events-none absolute inset-0 bg-background [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]" />
{/* Content */}
</div>Animated Spotlight
For premium hero sections. Requires motion:
"use client"
import { motion } from "motion/react"
import { cn } from "@/lib/utils"
export function Spotlight({ className }: { className?: string }) {
return (
<motion.div
className={cn(
"pointer-events-none fixed inset-0 -z-10 overflow-hidden",
className
)}
aria-hidden
>
<motion.div
className="absolute top-0 left-1/2 h-[60vh] w-[80vw] -translate-x-1/2 rounded-full opacity-20 blur-3xl"
style={{
background:
"radial-gradient(ellipse, color-mix(in oklab, var(--primary) 30%, transparent), transparent 70%)",
}}
animate={{ x: ["-10%", "10%", "-10%"] }}
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
/>
</motion.div>
)
}Combine with DotBackground for depth:
<div className="relative min-h-screen bg-background dark:bg-black">
<Spotlight />
<DotBackground className="absolute inset-0 opacity-30" />
<div className="relative z-10">{children}</div>
</div>For more creative direction (custom textures, particles, dramatic effects), apply /frontend-design thinking.
Section Wrapper
For sections that need different theme context:
type SectionProps = {
children: React.ReactNode
variant?: "default" | "muted" | "inverted"
className?: string
}
export function Section({ children, variant = "default", className }: SectionProps) {
return (
<section
className={cn(
"relative py-24",
variant === "muted" && "bg-muted",
variant === "inverted" && "bg-foreground text-background [&_*]:border-background/20",
className
)}
>
{children}
</section>
)
}Background Decision Tree
Full-page ambient effect?
├── Static → Fixed radial gradient (GradientHero)
├── Animated → Spotlight + DotBackground
├── Premium → Apply /frontend-design thinking
└── No ↓
Subtle texture for depth?
├── Grid → Technical/dashboard feel
├── Dots → Softer/organic feel
└── No ↓
Section contrast needed?
├── Yes → Section wrapper with variant
└── No → Standard bg-backgroundFile Organization
components/
├── ui/ # shadcn primitives
├── backgrounds/ # Grid, Dot, Gradient patterns
└── animations/ # FadeIn, ScrollRevealOptional Utilities
Scrollbar Hide
Hide scrollbar while preserving scroll functionality:
bun add tailwind-scrollbar-hide/* globals.css (Tailwind v4 — no config file needed) */
@import "tailwind-scrollbar-hide/v4";<div className="overflow-y-auto scrollbar-hide">
{/* Scrollable content without visible scrollbar */}
</div>Related skills
How it compares
Pick nextjs-shadcn over generic React skills when the stack is Next.js App Router plus shadcn/ui and RSC boundary mistakes are the main risk.
FAQ
When should nextjs-shadcn add use client?
nextjs-shadcn adds `"use client"` only at the smallest leaf component that needs state, effects, or browser APIs. Parent layouts and data-fetching shells stay Server Components by default, keeping client JavaScript bundles minimal in App Router projects.
What props can cross the server-client boundary?
nextjs-shadcn allows plain objects, arrays, and Server Actions as props across the server-client boundary. Functions, classes, and other non-serializable values are rejected, matching Next.js RSC serialization rules and preventing common AI hallucinations.
Is Nextjs Shadcn safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.