roduction baseline must reject implicit any types, enforce explicit return types, and validate index access.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Rationale: noUncheckedIndexedAccess prevents undefined crashes when accessing arrays or records. exactOptionalPropertyTypes catches accidental undefined assignments. These flags shift error detection from runtime to compile time, drastically reducing debugging cycles.
Step 2: Implement Flash-Free Dark Mode
CSS-only theming fails because the browser paints the default light theme before the stylesheet loads, causing a visible flash. The correct approach uses a blocking inline script that reads localStorage or system preferences before the DOM renders.
// src/lib/theme-script.ts
export const getThemeScript = () => {
return `
(function() {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored || (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
`;
};
Inject this script in the root layout before any React hydration occurs:
// src/app/layout.tsx
import { getThemeScript } from '@/lib/theme-script';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: getThemeScript() }} />
</head>
<body className={inter.className}>
{children}
</body>
</html>
);
}
Rationale: The inline script executes synchronously during HTML parsing. By the time React hydrates, the data-theme attribute is already applied, eliminating FOUC. suppressHydrationWarning is scoped to <html> to prevent mismatch errors during theme transitions.
Step 3: Structure for Static-First Rendering
The App Router defaults to server components. Production baselines should preserve this default and only opt into client-side rendering when interactivity is unavoidable.
// src/app/dashboard/page.tsx
import { DashboardMetrics } from '@/components/dashboard/metrics';
import { UserTable } from '@/components/dashboard/user-table';
export const dynamic = 'force-static';
export const revalidate = 3600;
export default async function DashboardPage() {
const metrics = await fetchMetrics();
const users = await fetchUsers();
return (
<main className="grid gap-6 p-6">
<DashboardMetrics data={metrics} />
<UserTable initialData={users} />
</main>
);
}
Rationale: force-static with explicit revalidation intervals ensures predictable caching behavior. Server components eliminate client-side JavaScript payload for data-fetching layers. Client components are isolated to interactive boundaries (forms, charts, modals), reducing hydration overhead.
Step 4: Token-Based Theming with Tailwind CSS v4
Tailwind v4 shifts configuration from JavaScript to CSS. Production theming should rely on CSS custom properties mapped to design tokens, enabling instant reskinning without touching component files.
/* src/app/globals.css */
@import "tailwindcss";
@theme {
--color-bg-primary: #ffffff;
--color-bg-secondary: #f8f9fa;
--color-text-primary: #0f172a;
--color-text-secondary: #475569;
--color-accent: #2563eb;
--color-border: #e2e8f0;
--font-sans: 'Inter', system-ui, sans-serif;
--font-serif: 'Fraunces', Georgia, serif;
--radius-lg: 0.75rem;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
[data-theme="dark"] {
--color-bg-primary: #0b0f19;
--color-bg-secondary: #111827;
--color-text-primary: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-accent: #38bdf8;
--color-border: #1e293b;
}
Rationale: CSS variables decouple design tokens from component logic. Switching palettes requires zero rebuilds. WCAG AA contrast ratios can be validated at the token level, ensuring compliance across all components automatically.
Pitfall Guide
1. Barrel File Overuse
Explanation: Re-exporting modules through index.ts files creates circular dependency risks and breaks tree-shaking. Bundlers cannot statically analyze re-exports reliably, shipping unused code to the client.
Fix: Import directly from source files. Use path aliases (@/components/ui/button) instead of barrel routes. Run madge --circular periodically to detect dependency loops.
2. CSS-Only Dark Mode Implementation
Explanation: Applying themes via CSS classes or media queries after hydration causes a visible flash between the default light theme and the intended dark theme. This degrades perceived performance and breaks accessibility expectations.
Fix: Use a blocking inline script that sets data-theme on <html> before React mounts. Pair with suppressHydrationWarning on the root element to prevent mismatch errors.
3. Implicit any and Loose Type Guards
Explanation: Disabling strict mode or using any to bypass compiler errors defeats TypeScript's purpose. Runtime type mismatches surface as silent failures or hydration crashes in Next.js.
Fix: Enforce strict: true in tsconfig.json. Use discriminant unions for state management and zod for runtime validation of external data. Never cast with as without explicit type guards.
4. Client-Side Bloat in App Router
Explanation: Marking entire pages as "use client" forces full hydration, increasing JavaScript payload and delaying Time to Interactive. Many developers default to client components out of habit rather than necessity.
Fix: Keep pages server components by default. Extract interactive logic into isolated client components. Use React.lazy and Suspense for heavy widgets. Audit bundle size with @next/bundle-analyzer.
5. Ignoring WCAG Contrast Ratios
Explanation: Design systems that prioritize aesthetics over accessibility fail compliance audits and exclude users with visual impairments. Low contrast ratios are a common cause of Lighthouse accessibility penalties.
Fix: Define color tokens with verified contrast ratios (4.5:1 for normal text, 3:1 for large text). Use automated tools like axe-core in CI pipelines. Test with browser dev tools' contrast checker before merging.
6. License Ambiguity in Third-Party Code
Explanation: Integrating community starters without verifying licensing terms can restrict commercial deployment, client work, or SaaS distribution. Many "free" templates carry restrictive clauses or require attribution.
Fix: Audit LICENSE files before integration. Prefer MIT, Apache 2.0, or explicit commercial licenses. Maintain a THIRD_PARTY_NOTICES.md file tracking dependencies and their terms.
7. Stale Dependency Chains
Explanation: Templates that haven't been updated in 6+ months drift from React 19, Next.js, and Tailwind v4 releases. Security patches, performance optimizations, and breaking changes accumulate, causing build failures.
Fix: Verify update cadence before adoption. Implement Dependabot or Renovate for automated PRs. Track upstream changelogs and schedule quarterly dependency audits.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Learning / Prototyping | Community starters or create-next-app | Fast iteration, low risk, acceptable debt | $0 upfront, 2β4 hrs cleanup |
| Client MVP / Agency Delivery | Production-grade baseline with strict TS & static defaults | Predictable delivery, compliance-ready, minimal debugging | $39β$150 one-time, saves 12β16 hrs dev time |
| Enterprise SaaS / Multi-tenant | Custom scaffold with enforced linting, CI gates, and design tokens | Scalability, security, long-term maintainability | Higher initial engineering cost, reduces technical debt by 60%+ |
| AI / Data-Heavy Dashboard | Server-first architecture + streaming + isolated client widgets | Handles large payloads, optimizes TTI, prevents hydration bottlenecks | Requires senior architecture, reduces infra costs via caching |
Configuration Template
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
poweredByHeader: false,
compress: true,
images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 60,
},
experimental: {
optimizePackageImports: ['@radix-ui/react-icons', 'lucide-react'],
},
};
export default nextConfig;
/* src/app/globals.css */
@import "tailwindcss";
@theme {
--color-surface: #ffffff;
--color-surface-hover: #f8fafc;
--color-text: #0f172a;
--color-text-muted: #64748b;
--color-primary: #0ea5e9;
--color-border: #e2e8f0;
--font-sans: 'Inter', system-ui, sans-serif;
--radius-md: 0.5rem;
--shadow-base: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
[data-theme="dark"] {
--color-surface: #0b0f19;
--color-surface-hover: #111827;
--color-text: #f1f5f9;
--color-text-muted: #94a3b8;
--color-primary: #38bdf8;
--color-border: #1e293b;
}
Quick Start Guide
- Initialize the project: Run
npx create-next-app@latest my-app --typescript --tailwind --app --src-dir --import-alias "@/*" to generate a strict, App Router-ready scaffold.
- Apply production configs: Replace the default
tsconfig.json and next.config.ts with the templates above. Update globals.css with the @theme block and dark mode variables.
- Inject the theme script: Create
src/lib/theme-script.ts with the blocking inline script logic. Import and inject it into src/app/layout.tsx inside <head>.
- Verify and deploy: Run
npm run build to confirm zero TypeScript errors. Test dark mode under 3G throttling in DevTools. Push to your hosting provider and run a Lighthouse audit to confirm 95+ scores across performance, accessibility, and SEO.