Frontend/New-App-Template/nextjs-tailwind-v4-luxe/SKILL.md
Comprehensive skill for building luxury-grade Next.js applications with Tailwind CSS v4, Radix UI (shadcn), and Framer Motion. Covers CSS-first theming, avant-garde UI design, code review, security audits, and performance optimization for high-end web experiences.
npx skillsauth add nordeim/prompt-engineering nextjs-tailwind-v4-luxeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Stack: Next.js 16+ • React 19+ • Tailwind CSS v4 • TypeScript • Radix UI (shadcn) • Framer Motion
Philosophy: Avant-Garde UI Design • Anti-Generic • Intentional Minimalism • WCAG AAA
Use this skill when:
Core Framework:
- Next.js: 16.1.4+ (App Router, Server Components, Turbopack)
- React: 19.2.3+
- TypeScript: 5.9.3+ (Strict Mode)
Styling & UI:
- Tailwind CSS: v4.1.18+ (CSS-first with @theme)
- Radix UI: Primitives for accessibility
- shadcn/ui: Component architecture
- Framer Motion: 12.29.0+ (Animations with reduced motion support)
- class-variance-authority: Component variants
- tailwind-merge: Class merging
- clsx: Conditional classes
Forms & Validation:
- react-hook-form: Form management
- zod: Schema validation
- @hookform/resolvers: Zod integration
Backend/Data:
- Prisma: ORM (optional)
- bcryptjs: Password hashing
- jose: JWT handling
- zod: Input validation
Development:
- ESLint: 9.x with TypeScript
- Prettier: 3.x with tailwindcss plugin
- Vitest: Unit testing
- Playwright: E2E testing
project-root/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── layout.tsx # Root layout, fonts, metadata
│ │ ├── page.tsx # Home page composition
│ │ ├── globals.css # Tailwind v4 theme + tokens
│ │ └── (routes)/ # Route groups
│ │
│ ├── components/
│ │ ├── layout/ # Navbar, Footer, Shell
│ │ ├── sections/ # Page sections (Hero, Features, etc.)
│ │ └── ui/ # Reusable UI primitives (shadcn)
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ ├── Card.tsx
│ │ └── ...
│ │
│ ├── lib/
│ │ ├── utils.ts # cn(), formatters, helpers
│ │ └── hooks/ # Custom React hooks
│ │ ├── useScrollSpy.ts
│ │ └── useReducedMotion.ts
│ │
│ ├── data/ # Static data (destinations, content)
│ └── types/ # Global TypeScript types
│
├── public/ # Static assets
├── docs/ # Design docs, guidelines
├── prisma/ # Database schema (if using)
├── next.config.ts # Next.js configuration
├── tsconfig.json # TypeScript strict config
└── package.json
Tailwind v4 uses CSS-only configuration. There should be NO tailwind.config.js or tailwind.config.ts file.
/* src/app/globals.css */
@import "tailwindcss";
/* ============================================
THEME CONFIGURATION
============================================ */
@theme {
/* Custom Colors */
--color-void: #050506;
--color-void-light: #0a0a0c;
--color-aurora-cyan: #22d3ee;
--color-aurora-purple: #a855f7;
--color-aurora-magenta: #ec4899;
--color-champagne: #c9b896;
--color-champagne-dark: #a89776;
/* Typography */
--font-sans: "Geist", "Inter", system-ui, sans-serif;
--font-serif: "Instrument Serif", "Georgia", serif;
/* Extended Spacing */
--spacing-18: 4.5rem;
--spacing-88: 22rem;
/* Custom Animations */
--animate-aurora-slow: aurora-slow 20s ease-in-out infinite;
--animate-float-slow: float-slow 25s ease-in-out infinite;
@keyframes aurora-slow {
0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.8; }
33% { transform: translate(30%, 20%) scale(1.1); opacity: 0.6; }
66% { transform: translate(-20%, 30%) scale(0.9); opacity: 0.7; }
}
@keyframes float-slow {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-20px) rotate(5deg); }
}
}
/* ============================================
BASE STYLES
============================================ */
@layer base {
* {
@apply border-slate-800;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-void text-slate-100 font-sans antialiased;
}
h1, h2, h3, h4, h5, h6 {
@apply font-serif;
}
}
/* ============================================
CUSTOM UTILITIES
============================================ */
@layer utilities {
.text-balance {
text-wrap: balance;
}
.glass-panel {
@apply bg-slate-900/30 backdrop-blur-xl border border-slate-800/50;
}
.aurora-gradient {
background: linear-gradient(
135deg,
var(--color-aurora-cyan) 0%,
var(--color-aurora-purple) 50%,
var(--color-aurora-magenta) 100%
);
}
}
// postcss.config.mjs
export default {
plugins: ["@tailwindcss/postcss"],
};
| v3 Utility | v4 Replacement |
|------------|----------------|
| bg-opacity-* | bg-color/* (e.g., bg-red-500/50) |
| text-opacity-* | text-color/* |
| shadow-sm | shadow-xs |
| shadow | shadow-sm |
| bg-gradient-to-r | bg-linear-to-r |
| outline-none | outline-hidden |
| ring | ring-3 |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
CSS Variable Syntax:
/* v3 */
<div class="bg-[--brand-color]">
/* v4 */
<div class="bg-(--brand-color)">
// src/components/ui/Button.tsx
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-champagne disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-champagne text-void hover:bg-champagne-dark",
outline: "border border-slate-700 bg-transparent hover:bg-slate-800",
ghost: "hover:bg-slate-800",
link: "underline-offset-4 hover:underline text-champagne",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-3 text-sm",
lg: "h-12 px-6 text-lg",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, children, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
disabled={props.disabled || loading}
{...props}
>
{loading ? (
<>
<span className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{children}
</>
) : (
children
)}
</button>
);
}
);
Button.displayName = "Button";
// src/components/sections/Hero.tsx
"use client";
import { motion } from "framer-motion";
import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
import { Button } from "@/components/ui/Button";
export function Hero() {
const prefersReducedMotion = useReducedMotion();
return (
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
{/* Background Animation */}
<motion.div
initial={prefersReducedMotion ? {} : { opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1, ease: "easeOut" }}
className="absolute inset-0 aurora-gradient opacity-20"
/>
{/* Content */}
<div className="relative z-10 container mx-auto px-4 text-center">
<motion.h1
initial={prefersReducedMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-5xl md:text-7xl lg:text-8xl font-serif text-white mb-6"
>
Beyond First Class
</motion.h1>
<motion.p
initial={prefersReducedMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="text-xl text-slate-400 max-w-2xl mx-auto mb-8"
>
Curated journeys for the world's most discerning travelers
</motion.p>
<motion.div
initial={prefersReducedMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
>
<Button size="lg">Begin Your Journey</Button>
</motion.div>
</div>
</section>
);
}
// src/components/ui/Input.tsx
import { forwardRef, type InputHTMLAttributes } from "react";
import { cn } from "@/lib/utils";
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, ...props }, ref) => {
return (
<div className="space-y-2">
{label && (
<label className="text-sm font-medium text-slate-300">
{label}
{props.required && <span className="text-aurora-magenta ml-1">*</span>}
</label>
)}
<input
ref={ref}
className={cn(
"flex h-10 w-full rounded-lg border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-champagne focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
error && "border-aurora-magenta focus:ring-aurora-magenta",
className
)}
{...props}
/>
{error && (
<p className="text-sm text-aurora-magenta">{error}</p>
)}
</div>
);
}
);
Input.displayName = "Input";
// src/lib/hooks/useReducedMotion.ts
import { useState, useEffect } from "react";
export function useReducedMotion(): boolean {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setPrefersReducedMotion(mediaQuery.matches);
const handler = (event: MediaQueryListEvent) => {
setPrefersReducedMotion(event.matches);
};
mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
}, []);
return prefersReducedMotion;
}
Every interface must have a distinctive conceptual direction. Reject:
Before coding, commit to a BOLD aesthetic direction:
| Direction | Characteristics | |-----------|-----------------| | Brutally Minimal | Extreme whitespace, single focal point | | Maximalist Chaos | Layered textures, bold typography | | Retro-Futuristic | Neon, chrome, geometric patterns | | Organic/Natural | Soft curves, earthy tones, fluid shapes | | Luxury/Refined | Serif fonts, gold accents, subtle gradients | | Editorial/Magazine | Asymmetric layouts, bold headlines | | Brutalist/Raw | Exposed structure, monospace, high contrast | | Art Deco/Geometric | Symmetry, gold/black, stepped forms |
Analyze every design decision through:
// Timing
const ANIMATION_DURATION = {
instant: 0, // State changes
fast: 150, // Micro-interactions
normal: 300, // Standard transitions
slow: 500, // Page transitions
dramatic: 800, // Hero animations
};
// Easing
const EASING = {
entrance: [0.0, 0.0, 0.2, 1], // Decelerate
exit: [0.4, 0.0, 1.0, 1.0], // Accelerate
standard: [0.4, 0.0, 0.2, 1], // Symmetric
};
// Stagger
const STAGGER_DELAY = 50; // ms between items
Before any code review or completion claim:
# TypeScript Check
npx tsc --noEmit
# Lint
npm run lint
# Tests
npm test
# Build
npm run build
any types - use unknown insteaduseReducedMotion check for all animationsdangerouslySetInnerHTML without sanitization)interface over type (except unions/intersections)READ → UNDERSTAND → VERIFY → EVALUATE → RESPOND → IMPLEMENT
Rules:
| Category | Checks |
|----------|--------|
| A01 Broken Access Control | IDOR prevention, proper auth checks, SSRF protection |
| A02 Security Misconfiguration | Secure headers, no default credentials, error handling |
| A03 Supply Chain | Audit dependencies (npm audit), lock file integrity |
| A04 Cryptographic Failures | bcrypt for passwords, jose for JWT, no hardcoded secrets |
| A05 Injection | No SQL injection (use Prisma/ORM), no XSS |
| A06 Insecure Design | Input validation, business logic flaws |
| A07 Authentication Failures | Session management, MFA, secure cookies |
| A08 Integrity Failures | Code signing, dependency verification |
| A09 Logging & Monitoring | Security event logging, failed auth attempts |
| A10 Exceptional Conditions | Fail-closed, proper error handling |
// ❌ String concatenation in queries
const query = "SELECT * FROM users WHERE id = " + userId;
// ❌ Dynamic code execution
eval(userInput);
new Function(userInput);
// ❌ Unsafe deserialization
JSON.parse(untrustedData); // Without validation
// ❌ Path traversal
fs.readFile(`./uploads/${userInput}`);
// ❌ Disabled security
fetch(url, { verify: false }); // SSL verification disabled
| Type | Indicators |
|------|------------|
| API Keys | api_key, apikey, high entropy strings |
| Tokens | token, bearer, jwt |
| Credentials | password, secret, key |
| Cloud | AWS_, AZURE_, GCP_ prefixes |
// next.config.ts
const nextConfig = {
// Security headers
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
],
},
];
},
// Image optimization
images: {
formats: ["image/avif", "image/webp"],
remotePatterns: [
{ protocol: "https", hostname: "images.unsplash.com" },
],
},
};
Eliminate Waterfalls
// ❌ Sequential (slow)
const user = await getUser();
const posts = await getPosts(user.id);
// ✅ Parallel (fast)
const [user, posts] = await Promise.all([
getUser(),
getPosts(),
]);
Bundle Size Optimization
// ✅ Dynamic imports for large components
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
});
// ✅ Direct imports (avoid barrel files)
import { Button } from "@/components/ui/Button";
// NOT: import { Button } from "@/components/ui"; (index.ts)
Server Components by Default
// Server component (default) - no "use client"
export async function ServerComponent() {
const data = await fetchData(); // Fetches on server
return <div>{data}</div>;
}
// Client component only when needed
"use client";
export function ClientComponent() {
const [state, setState] = useState();
return <div>{state}</div>;
}
import Image from "next/image";
// ✅ Optimized images
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Above-fold images
className="object-cover"
/>;
// ✅ Responsive images
<Image
src="/photo.jpg"
alt="Photo"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
// app/layout.tsx
import { Geist, Instrument_Serif } from "next/font/google";
const geist = Geist({
subsets: ["latin"],
variable: "--font-sans",
});
const instrumentSerif = Instrument_Serif({
subsets: ["latin"],
weight: "400",
variable: "--font-serif",
});
export default function RootLayout({ children }) {
return (
<html className={`${geist.variable} ${instrumentSerif.variable}`}>
<body>{children}</body>
</html>
);
}
// ✅ Semantic HTML
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
<main>
<section aria-labelledby="features-heading">
<h2 id="features-heading">Features</h2>
</section>
</main>
// ✅ Focus management
<button className="focus-visible:ring-2 focus-visible:ring-champagne focus-visible:outline-none">
Click me
</button>
// ✅ ARIA labels for icon buttons
<button aria-label="Close dialog">
<XIcon />
</button>
// ✅ Form labels
<label htmlFor="email">Email</label>
<input id="email" type="email" aria-required="true" />
// ✅ Error announcements
<div role="alert" className="text-aurora-magenta">
{errorMessage}
</div>
| Element | Minimum Ratio | |---------|---------------| | Normal text | 4.5:1 | | Large text (18pt+) | 3:1 | | UI components | 3:1 |
Test with: WebAIM Contrast Checker
"use client";
import { motion } from "framer-motion";
import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
export function AnimatedCard() {
const prefersReducedMotion = useReducedMotion();
return (
<motion.div
initial={prefersReducedMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
Content
</motion.div>
);
}
// Desktop nav: visible at md and above
<nav className="hidden md:flex items-center gap-8">...</nav>
// Mobile trigger: hidden at md and above
<button className="md:hidden" aria-label="Open menu">Menu</button>
"use client";
import * as React from "react";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/Sheet";
export function MobileNav() {
const [open, setOpen] = React.useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<MenuIcon />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px]">
<nav className="flex flex-col gap-4">
<Link href="/" onClick={() => setOpen(false)}>Home</Link>
<Link href="/about" onClick={() => setOpen(false)}>About</Link>
</nav>
</SheetContent>
</Sheet>
);
}
| Class | Symptom | Fix |
|-------|---------|-----|
| A | No visible nav on mobile | Add mobile trigger + overlay |
| B | Hidden by opacity/visibility | Verify state toggling |
| C | Clipped by overflow | Use position: fixed |
| D | Behind another layer | Check z-index scale |
| E | Breakpoint mismatch | Verify viewport meta |
| F | JavaScript failure | Guard selectors, check console |
| G | Keyboard inaccessible | Use real <button> elements |
| H | Click-outside race | Exclude trigger from handler |
# 1. Type Check
npx tsc --noEmit
# 2. Lint
npm run lint
# 3. Tests
npm test
# 4. Build
npm run build
# 5. Security Audit
npm audit
Before marking any UI work complete:
prefers-reduced-motion| Type | Pattern | Example |
|------|---------|---------|
| Components | PascalCase | Hero.tsx, Button.tsx |
| Hooks | camelCase with use | useScrollSpy.ts |
| Utils | camelCase | cn.ts, formatCurrency.ts |
| Constants | SCREAMING_SNAKE | API_ENDPOINTS |
| Types | PascalCase | UserProfile, Destination |
| Files | kebab-case | concierge-form.tsx |
// 1. React/Next
import { useState } from "react";
import Image from "next/image";
// 2. Third-party
import { motion } from "framer-motion";
import { z } from "zod";
// 3. Absolute imports (@/)
import { Button } from "@/components/ui/Button";
import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
// 4. Relative imports
import { HeroAnimation } from "./HeroAnimation";
"use client"; // If using hooks/browser APIs
// Or for server components (default), no directive needed
| Skill | When to Use | |-------|-------------| | aesthetic | Deep design analysis, inspiration workflows | | code-review | Detailed review protocols, feedback handling | | vulnerability-scanner | Deep security analysis, threat modeling | | nextjs-react-expert | Advanced performance optimization | | web-design-guidelines | Vercel Web Interface Guidelines compliance | | tailwind-patterns | Tailwind CSS patterns and best practices | | ui-styling | shadcn/ui implementation guidance |
Remember: Technical excellence requires both rigorous implementation and distinctive design. Every line of code and every pixel should demonstrate intentionality and craftsmanship worthy of a luxury experience.
development
Performs comprehensive enterprise-grade critical code review on project folders or GitHub repositories, focusing on quality, security, performance, maintainability, and best practices
development
Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas
development
Build modern full-stack web applications with Next.js (App Router, Server Components, RSC, PPR, SSR, SSG, ISR), Turborepo (monorepo management, task pipelines, remote caching, parallel execution), and RemixIcon (3100+ SVG icons in outlined/filled styles). Use when creating React applications, implementing server-side rendering, setting up monorepos with multiple packages, optimizing build performance and caching strategies, adding icon libraries, managing shared dependencies, or working with TypeScript full-stack projects.
tools
Create beautiful, accessible user interfaces with shadcn/ui components (built on Radix UI + Tailwind), Tailwind CSS utility-first styling, and canvas-based visual designs. Use when building user interfaces, implementing design systems, creating responsive layouts, adding accessible components (dialogs, dropdowns, forms, tables), customizing themes and colors, implementing dark mode, generating visual designs and posters, or establishing consistent styling patterns across applications.