Intro
Every time I start a new Next.js project, I find myself going through the same setup routine. Instead of constantly googling "how to configure Prettier with Next.js" or "Emotion cache provider setup" for the hundredth time, I decided to document my go-to configuration once and for all.
This is my personal checklist that gets me from create-next-app
to a fully configured development environment. Maybe it'll save you some time too.
Quick Package Installation
Before diving into the configs, here are all the packages you'll need:
1# Runtime dependencies2npm install @emotion/css @emotion/react @emotion/server @emotion/styled34# Development dependencies5npm install -D eslint eslint-config-next eslint-config-prettier eslint-plugin-unused-imports @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier stylelint stylelint-config-recess-order stylelint-config-standard stylelint-order stylelint-no-unsupported-browser-features postcss-styled-syntax
1. Set up Prettier
First things first - consistent code formatting. I always start with Prettier because it eliminates so many formatting debates.
Install packages:
1npm install -D prettier eslint-config-prettier
Create prettier.config.js
:
1/** @type {import('prettier').Config} */2module.exports = {3 tabWidth: 2,4 semi: false,5 singleQuote: true,6 trailingComma: 'all',7}
This config gives you clean, consistent formatting with no semicolons and trailing commas everywhere (which makes for cleaner git diffs).
2. Configure ESLint
ESLint is where the magic happens - it catches bugs before they make it to production and enforces consistency across your codebase. Here's my battle-tested configuration that focuses on meaningful rules with clear purposes.
Install packages:
1npm install -D eslint eslint-config-next eslint-config-prettier eslint-plugin-unused-imports @typescript-eslint/eslint-plugin @typescript-eslint/parser
Create eslint.config.js
:
1import path from 'node:path'2import { fileURLToPath } from 'node:url'3import typescriptEslint from '@typescript-eslint/eslint-plugin'4import typescriptEslintParser from '@typescript-eslint/parser'56import { FlatCompat } from '@eslint/eslintrc'7import js from '@eslint/js'8import unusedImports from 'eslint-plugin-unused-imports'910const __filename = fileURLToPath(import.meta.url)11const __dirname = path.dirname(__filename)12const compat = new FlatCompat({13 baseDirectory: __dirname,14 recommendedConfig: js.configs.recommended,15 allConfig: js.configs.all,16})1718// Shared rules for all files19const sharedRules = {20 // Performance & Bundle Size21 'unused-imports/no-unused-imports': 'error', // Unused imports increase bundle size22 'no-unused-vars': 'off', // Turn off base rule to avoid conflicts23 'unused-imports/no-unused-vars': [24 'warn',25 {26 vars: 'all',27 varsIgnorePattern: '^_',28 args: 'after-used',29 argsIgnorePattern: '^_',30 },31 ],3233 // Code Quality & Bug Prevention34 'no-console': ['error', { allow: ['warn', 'dir'] }], // Prevent console.log in production, allow debugging tools35 'no-restricted-syntax': ['error', 'TSEnumDeclaration', 'WithStatement'], // Prevent problematic syntax patterns36 'no-unused-expressions': 'error', // Catch typos like `foo.bar` instead of `foo.bar()`37 'no-debugger': 'error', // Prevent debugger statements in production38 'no-alert': 'error', // Prevent alert() usage in production39 'no-var': 'error', // Enforce let/const over var40 'prefer-const': 'error', // Use const when variables aren't reassigned4142 // TypeScript rules (turned off for compatibility)43 '@typescript-eslint/interface-name-prefix': 'off', // Allow flexible interface naming44 '@typescript-eslint/member-delimiter-style': 'off', // Don't enforce semicolons in interfaces45 '@typescript-eslint/no-empty-interface': 'off', // Sometimes empty interfaces are useful as placeholders4647 // React Best Practices48 'react/self-closing-comp': ['error', { component: true, html: true }], // <Component /> instead of <Component></Component>4950 // Import Organization - Clean imports = easier navigation51 'import/order': [52 'error',53 {54 groups: [55 'builtin', // node:fs, node:path56 'external', // react, next, lodash57 'type', // import type { ... }58 'internal', // @/components, ~/utils59 'sibling', // ./component60 'parent', // ../component61 'index', // ./62 'object', // import log = console.log63 ],64 'newlines-between': 'always',65 pathGroupsExcludedImportTypes: ['react'],66 alphabetize: { order: 'asc', caseInsensitive: true },67 pathGroups: [{ pattern: 'react', group: 'external', position: 'before' }], // React always goes first68 },69 ],70}7172const config = [73 ...compat.extends('next/core-web-vitals'),74 {75 files: ['**/*.{js,jsx,ts,tsx}'],76 languageOptions: {77 parser: typescriptEslintParser,78 parserOptions: {79 ecmaVersion: 'latest',80 sourceType: 'module',81 ecmaFeatures: { jsx: true },82 },83 },84 plugins: {85 'unused-imports': unusedImports,86 '@typescript-eslint': typescriptEslint,87 },88 rules: sharedRules,89 },90]9192export default config
Why These Rules Matter
Bundle Size & Performance:
unused-imports/no-unused-imports
- Unused imports get bundled even if never used, bloating your JavaScript bundle. This catches imports you forgot to remove after refactoring.no-unused-vars
- Dead code elimination works better when there are no unused variables lingering around.
Bug Prevention:
no-console
- Console logs in production can leak sensitive info and slow down performance. We allowwarn
anddir
for debugging.no-restricted-syntax
- TypeScript enums can cause unexpected behavior and bundle bloat;with
statements are just evil.prefer-const
- Catches bugs where you accidentally reassign variables that should be constants.
Developer Experience:
import/order
- Consistent import ordering makes it easier to find what you're looking for and reduces merge conflicts. React always goes first, then external packages, then your internal code, all alphabetized.react/self-closing-comp
-<Button active />
is cleaner than<Button active></Button>
The config is intentionally minimal - it focuses on catching real bugs and maintaining consistency without being overly restrictive. Sometimes the best ESLint config is the one that stays out of your way while preventing actual problems.
3. Install and Configure Emotion
For styling, I use Emotion. It gives me the flexibility of CSS-in-JS with great performance and TypeScript support.
Install packages:
1npm install @emotion/css @emotion/react @emotion/server @emotion/styled
Setting up EmotionRegistry (Optional but Recommended)
The EmotionRegistry is technically optional, but I highly recommend it. Without it, you'll get that dreaded "flash of unstyled content" (FOUC) where your page looks broken for a split second on initial load. The registry ensures your CSS is properly cached and rendered server-side, so users see a fully styled page immediately.
Create lib/EmotionRegistry.tsx
:
1'use client'23import { useState } from 'react'4import type { ReactNode } from 'react'56import createCache from '@emotion/cache'7import { CacheProvider } from '@emotion/react'8import { useServerInsertedHTML } from 'next/navigation'910export default function EmotionRegistry({ children }: { children: ReactNode }) {11 const [cache] = useState(() => {12 const cache = createCache({13 key: 'css',14 prepend: true,15 })16 cache.compat = true17 return cache18 })1920 useServerInsertedHTML(() => {21 return (22 <style23 data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}24 dangerouslySetInnerHTML={{25 __html: Object.values(cache.inserted).join(' '),26 }}27 />28 )29 })3031 return <CacheProvider value={cache}>{children}</CacheProvider>32}
Add to layout.tsx
:
1import EmotionRegistry from './lib/EmotionRegistry'23export default function RootLayout({4 children,5}: {6 children: React.ReactNode7}) {8 return (9 <html lang="en">10 <body>11 <EmotionRegistry>{children}</EmotionRegistry>12 </body>13 </html>14 )15}
4. Configure Stylelint for CSS-in-JS
Even with CSS-in-JS, you still want to catch CSS mistakes and maintain consistency. Stylelint works great with Emotion through the postcss-styled-syntax
plugin, and these rules are specifically tuned for performance and CSS-in-JS compatibility.
Install packages:
1npm install -D stylelint stylelint-config-standard stylelint-config-recess-order stylelint-order stylelint-no-unsupported-browser-features postcss-styled-syntax
Create stylelint.config.js
:
1/** @type {import('stylelint').Config} */2module.exports = {3 extends: ['stylelint-config-standard', 'stylelint-config-recess-order'],4 plugins: ['stylelint-order', 'stylelint-no-unsupported-browser-features'],5 customSyntax: 'postcss-styled-syntax', // This makes Stylelint understand CSS-in-JS6 ignoreFiles: ['**/node_modules/**', 'src/styles/resetCss/resetCss.ts'],7 rules: {8 // CSS-in-JS compatibility - these rules don't play well with CSS-in-JS9 'selector-class-pattern': null, // CSS-in-JS generates class names dynamically10 'alpha-value-notation': null, // Modern alpha notation can break in some contexts11 'color-function-notation': 'legacy', // Use rgb() instead of rgb(255 0 0) for better compatibility12 'no-descending-specificity': null, // CSS-in-JS handles specificity differently13 'property-no-vendor-prefix': null, // Sometimes you need vendor prefixes for cutting-edge features14 'value-no-vendor-prefix': null,15 'nesting-selector-no-missing-scoping-root': null, // CSS-in-JS handles scoping automatically1617 /* --- Performance & Bundle Size --- */18 // Block render-blocking @import in CSS - use JavaScript imports instead19 'at-rule-disallowed-list': ['import'],2021 // Keep CSS small & selector matching fast22 'selector-max-compound-selectors': 4, // .a .b .c .d is OK; deeper selectors hurt performance23 'selector-max-combinators': 3, // Limit descendant/child/sibling chains24 'selector-max-specificity': '0,4,0', // Avoid overly specific selectors that are hard to override25 'selector-max-class': 4, // Too many classes in one selector gets unwieldy26 'selector-max-id': 0, // IDs have too much specificity and aren't reusable27 'selector-max-universal': 0, // Universal selectors (*) are performance killers28 'selector-no-qualifying-type': [true, { ignore: ['attribute', 'class'] }], // div.class is redundant2930 // Avoid CSS mistakes that bloat bundles31 'declaration-block-no-duplicate-properties': [32 true,33 { ignore: ['consecutive-duplicates-with-different-values'] }, // Allow fallbacks like display: flex; display: grid;34 ],35 'declaration-block-no-shorthand-property-overrides': true, // margin-top shouldn't override margin36 'declaration-no-important': true, // !important makes CSS harder to maintain and debug3738 // Tame nesting (important for CSS-in-JS readability)39 'max-nesting-depth': 3, // Deep nesting creates specificity wars and hard-to-debug CSS4041 // Warn about features that need heavy polyfills (keeps bundles lean)42 'plugin/no-unsupported-browser-features': [43 true,44 {45 severity: 'warning', // Warn but don't block - sometimes cutting-edge features are worth it46 ignorePartialSupport: true, // Don't warn if feature has partial support47 },48 ],49 },50}
Create .browserslistrc
:
1last 2 chrome version2last 2 firefox version3last 2 Edge version4last 2 Safari version5iOS >= 186not dead
This file tells the no-unsupported-browser-features
plugin which browsers you're targeting, so it can warn you about CSS features that won't work in your supported browsers. The configuration above covers modern browsers while ensuring iOS 18+ compatibility for mobile users.
Why Stylelint Matters for CSS-in-JS
Performance Focus: The selector limits (selector-max-compound-selectors
, selector-max-specificity
) prevent performance-killing CSS that makes browsers work harder to match elements. Even with CSS-in-JS, badly structured CSS can slow down your app.
Bundle Size: Rules like at-rule-disallowed-list: ['import']
prevent render-blocking CSS imports, and declaration-no-important
discourages CSS that's hard to optimize.
CSS-in-JS Compatibility: The disabled rules (selector-class-pattern
, no-descending-specificity
) acknowledge that CSS-in-JS tools like Emotion handle these concerns automatically through scoping and dynamic class generation.
Browser Support: The no-unsupported-browser-features
plugin warns when you use CSS features that might need polyfills, helping you make informed decisions about bundle size vs. cutting-edge features.
5. Update Next.js Configuration
This is where the magic happens - configuring Next.js to work seamlessly with Emotion using SWC, Next.js's native Rust-based compiler (not Babel). SWC is significantly faster than Babel and comes built-in with Next.js, so we get excellent performance out of the box.
Update next.config.ts
:
1import type { NextConfig } from 'next'23const nextConfig: NextConfig = {4 // Disable cache in development for faster iteration5 ...(process.env.NODE_ENV !== 'production' && {6 cacheMaxMemorySize: 0,7 }),89 // SWC compiler configuration for Emotion10 compiler: {11 emotion: {12 // Source maps in development only - helps with debugging CSS-in-JS13 sourceMap: process.env.NODE_ENV !== 'production',14 // Auto-label components in development for easier debugging15 autoLabel: process.env.NODE_ENV !== 'production' ? 'always' : 'never',16 // Format for auto-generated labels17 labelFormat: '[local]',18 // Import map optimization - lets you import styled from @emotion/react19 importMap: {20 '@emotion/react': {21 styled: {22 canonicalImport: ['@emotion/styled', 'default'],23 },24 },25 },26 },27 },2829 // Additional optimizations you might want to add:30 experimental: {31 // Enable optimizeCss for better CSS bundling (optional)32 optimizeCss: true,33 },34}3536export default nextConfig
Why This Configuration Matters
SWC vs Babel: This config uses SWC (Speedy Web Compiler), Next.js's built-in Rust-based compiler, instead of Babel. SWC is up to 20x faster than Babel and requires zero additional setup. The compiler.emotion
field tells SWC how to handle Emotion's CSS-in-JS transforms.
Development vs Production:
- In development: Source maps and auto-labeling help you debug which components are generating which styles
- In production: These features are disabled for smaller bundles and better performance
Import Map Optimization: The import map allows you to write import styled from '@emotion/react'
instead of import styled from '@emotion/styled'
, which can be more convenient and consistent with other Emotion imports.
Cache Configuration: Disabling cache in development prevents stale build artifacts from causing confusion during development.
6. Set up Theme System
Create lib/theme.d.ts
:
1import '@emotion/react'23declare module '@emotion/react' {4 export interface Theme {5 colors: {6 background: string7 text: string8 muted: string9 floating: string10 primary: string11 }12 }13}
Create lib/theme.ts
:
1import type { Theme } from '@emotion/react'23export const colors: Theme = {4 colors: {5 background: 'hsl(0deg, 0%, 100%)',6 text: 'hsl(230deg, 30%, 15%)',7 muted: 'hsl(220deg, 20%, 85%)',8 floating: 'hsl(0deg, 0%, 95%)',9 primary: 'hsl(325deg, 90%, 72%)',10 },11}
7. Add CSS Reset
Create lib/reset.ts
:
1import { css } from '@emotion/react'23export const resetCss = css`4 *,5 *::before,6 *::after {7 box-sizing: border-box;8 }910 * {11 margin: 0;12 padding: 0;13 }1415 body {16 line-height: 1.5;17 -webkit-font-smoothing: antialiased;18 }1920 img,21 picture,22 video,23 canvas,24 svg {25 display: block;26 max-width: 100%;27 }2829 input,30 button,31 textarea,32 select {33 padding: 0;34 font: inherit;35 color: inherit;36 text-align: inherit;37 text-transform: inherit;38 vertical-align: middle;39 background: transparent;40 border-style: none;41 }4243 li {44 list-style: none;45 }4647 p,48 h1,49 h2,50 h3,51 h4,52 h5,53 h6 {54 overflow-wrap: break-word;55 }5657 a {58 color: inherit;59 text-decoration: none;60 }6162 #root,63 #__next {64 isolation: isolate;65 }66`
This reset gives you a clean foundation to build on, with sensible defaults that work well across different browsers.
Why This Stack?
This setup might seem like overkill for a simple project, but each piece serves a purpose:
- Prettier eliminates formatting discussions and keeps code consistent
- ESLint catches bugs early and enforces good practices
- Emotion provides powerful styling capabilities with TypeScript support
- Stylelint ensures your CSS-in-JS is performant and maintainable
- The reset gives you a predictable starting point across browsers
The best part? Once you have this setup, you can copy these configs to any new Next.js project and be productive immediately.
Wrapping Up
That's my complete Next.js setup checklist. It takes about 15 minutes to get through all these steps, but it saves hours of debugging and configuration headaches later.
Feel free to adjust the configurations to match your preferences - the important thing is having a consistent starting point that you can rely on project after project.