7 min read

Published Updated

My Prettier Config for Next.js and Tailwind, Option by Option

The Prettier setup I run on React and Next.js apps: three deviations from the defaults, import groups that match the folder tree, Tailwind class sorting, and the sort-imports plugin migration I did halfway through.

This is the Prettier config I use on front-end work, taken from my own repos rather than assembled for a blog post. The interesting part is not the file, it is which lines are deviations from Prettier's defaults and why each one survives. There are three.

Start with the hub post if you want the reasoning about enforcement points and pinned versions. This one is the config.

The config

{
  "singleQuote": true,
  "printWidth": 100,
  "singleAttributePerLine": true,
  "plugins": [
    "@ianvs/prettier-plugin-sort-imports",
    "prettier-plugin-tailwindcss"
  ],
  "importOrder": [
    "<BUILTIN_MODULES>",
    "",
    "^react$",
    "^next(/.*)?$",
    "",
    "<THIRD_PARTY_MODULES>",
    "",
    "^@/design-system/(.*)$",
    "^@/components/ui/(.*)$",
    "^@/components/(.*)$",
    "",
    "^@/hooks/(.*)$",
    "^@/lib/(.*)$",
    "^@/utils/(.*)$",
    "^@/helpers/(.*)$",
    "",
    "^@/context/(.*)$",
    "^@/actions/(.*)$",
    "^@/auth/(.*)$",
    "^@/env/(.*)$",
    "",
    "^[./]"
  ],
  "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"],
  "importOrderTypeScriptVersion": "5.0.0",
  "importOrderCaseSensitive": false,
  "tailwindStylesheet": "./app/globals.css",
  "tailwindFunctions": ["clsx", "cn", "cva"]
}

That is the whole file. Notice what is absent: semi, trailingComma, tabWidth, endOfLine, bracketSpacing, arrowParens. Those are all Prettier defaults (true, "all", 2, "lf", true, "always" respectively) and writing them down only creates the impression that they were debated. trailingComma became "all" in Prettier 3 and endOfLine became "lf" in version 2, so a config still spelling out "es5" or "auto" is usually a config nobody has read since 2022.

The three deviations

singleQuote: true. Prettier defaults to double quotes. This one is pure habit: I have written single-quoted JavaScript for years and double-quoted files look like somebody else's code to me. I am not going to pretend it is better. It is consistent, it is one line, and JSX attributes stay double-quoted regardless because jsxSingleQuote is separately false.

printWidth: 100. The default is 80. Eighty was chosen for terminals and it shows on modern React code, where a component with three typed props and a generic hits the limit before it says anything. At 100, Prettier wraps less often, which matters because most of Prettier's diff noise comes from wrapping decisions flipping when a line crosses the threshold. I do not go to 120: past about 100 characters, side-by-side review on a laptop starts truncating, and I review more code than I write.

singleAttributePerLine: true. Off by default. With it on, any JSX element that does not fit on one line gets one attribute per line instead of packing them. It makes JSX diffs read like a list: adding a prop is one added line, not a reflow of three. This is the one option where I think the non-default is objectively better for diffs, and it is also the one people dislike most, because tall components look tall.

Import order that matches the folder tree

The groups above are not alphabetical categories, they are a dependency gradient: runtime, framework, third party, presentational code, logic, app wiring, then relative paths. Reading the top of a file tells you what layer the file lives in before you read the body.

The mechanics come from @ianvs/prettier-plugin-sort-imports:

  • Each string is a regex matched against the import source. <BUILTIN_MODULES> and <THIRD_PARTY_MODULES> are special tokens, and the plugin injects both if you leave them out, so anything unmatched lands in the third-party bucket rather than disappearing.
  • An empty string inserts a blank line. That is the only separation mechanism in this plugin, which is why there is no importOrderSeparation option to set.
  • Sorting specifiers inside the braces, merging two imports from the same source, and combining import type with a value import from the same module are all automatic. The last one requires importOrderTypeScriptVersion to be 4.5.0 or higher, which is why that line is in the config and not decoration.
  • importOrderParserPlugins is passed to Babel so it can parse the file. TypeScript and JSX are the defaults, and I add decorators-legacy because a couple of my repos share types with backends that use decorators.

The plugin does not reorder across side-effect imports, which is the reason it is safe to run on save. If you have an import that must run before another one (a polyfill, server-only), it stays put unless you list it in importOrderSafeSideEffects.

Tailwind class sorting

prettier-plugin-tailwindcss sorts the classes inside className into Tailwind's own order and drops duplicates. The value is not aesthetic. When class order is deterministic, two developers writing the same element produce the same string, and a conflicting pair like p-4 p-8 ends up adjacent where you can see it.

Two things will silently not work if you get them wrong:

  • It must be last in the plugins array. The plugin uses a Prettier API that only one plugin can hold, and the sort-imports plugin uses it too. If Tailwind is not last, one of the two stops running and nothing warns you.
  • On Tailwind 4, you need tailwindStylesheet pointing at your CSS entry point, not tailwindConfig. Version 4 moved the theme into CSS, so the plugin reads your stylesheet to learn your custom utilities. Paths resolve relative to the Prettier config file. tailwindFunctions extends sorting to clsx, cn and cva arguments, which is where most of my class strings actually live.

The plugin migration, honestly

This site still runs @trivago/prettier-plugin-sort-imports, with importOrderSeparation: true and importOrderSortSpecifiers: true. My newer repos run @ianvs. Both plugins are maintained, and the @ianvs one is a fork of the Trivago plugin, so if you only ever read one config of mine you would conclude I use whichever one you happened to read. I would rather say plainly: @ianvs is what I would pick today, and moving the older repos over is a chore I have not finished.

The reason to prefer @ianvs is control over blank lines. With Trivago, importOrderSeparation: true puts a blank line between every group, so a twelve-group order produces twelve stanzas of one or two imports. With @ianvs you place "" exactly where you want the breaks, which is how the config above gets five readable blocks out of thirteen patterns.

If you migrate, the option names change and the old ones are silently ignored rather than rejected:

Trivago option@ianvs equivalent
importOrderSeparation: true"" entries placed inside importOrder
importOrderSortSpecifiersalways on
importOrderCaseInsensitive: trueimportOrderCaseSensitive: false (the default)
importOrderBuiltinModulesToTopa <BUILTIN_MODULES> entry at the position you want

That last row is worth calling out because importOrderBuiltinModulesToTop is a name that circulates in blog posts and example configs while being an option of neither plugin's current documentation. Both use the <BUILTIN_MODULES> token instead. An unrecognized key in .prettierrc does not fail, it just does nothing, which is how a config accumulates lines that have never had an effect.

Wiring

Editor. Prettier's own editor page covers the setup. In VS Code that is the Prettier extension plus "editor.formatOnSave": true, committed in .vscode/settings.json so it applies to anyone who opens the repo rather than to whoever configured it once.

ESLint. My flat config is short on purpose:

import nextVitals from 'eslint-config-next/core-web-vitals';
import { defineConfig, globalIgnores } from 'eslint/config';

export default defineConfig([
  ...nextVitals,
  globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
]);

No formatting rules, so there is nothing for Prettier to fight and no need for eslint-config-prettier. Add it the moment your config pulls in a plugin that has stylistic rules, because two tools disagreeing about the same character is an infinite loop with a human in it.

Commit hook. Husky runs lint-staged, so only staged files are touched:

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": "eslint --cache --fix",
    "*.{js,jsx,ts,tsx,css,md,mdx,json,yml,yaml}": "prettier --write"
  }
}

ESLint first with --fix, then Prettier, so Prettier gets the final say on whitespace after any autofix rewrites code.

CI. prettier --check . and eslint .. Check, not write.

Ignores. Keep a .prettierignore even though Prettier already skips node_modules, version control directories, and anything in a .gitignore in the directory it runs from. Mine adds build output and coverage. The reason to have the file at all is that prettier --write . should be safe to type without thinking about generated files.

Pitfalls

  • Formatting a whole repo in a feature branch. Do it as its own commit on its own, add the hash to .git-blame-ignore-revs, and merge it before anyone rebases onto it.
  • Per-file overrides. // prettier-ignore exists and I use it about once a year, for a hand-aligned lookup table. If you need it often, the option you disagree with is worth changing globally instead.
  • Two configs in one monorepo. One .prettierrc at the root with an overrides block beats one per package. I learned that after a shared package started reformatting itself depending on which app's script ran it.
  • Unpinned plugin versions. The formatter and its plugins are one unit. A caret on the Tailwind plugin is a class reorder waiting to land in someone else's pull request.

The back-end config is this file with the front-end plugins removed, and the differences are smaller than you would expect: back-end JavaScript and TypeScript. For Python, none of this applies and the toolchain is one binary: Python formatting.

Written by

Software Engineer II at Spyne. I build production voice agents, telephony infrastructure, and developer tools, and contribute to the LiveKit Agents SDK.

Experience and open-source contributions →
The Next.js Naming Conventions I Actually FollowI audited twelve of my own Next.js repos before writing this down. Here are the naming rules that survived the audit, the two places my repos disagree with each other, and the rules the framework decides for you.May 2025Formatting Belongs to Tools, Not Code ReviewsOne formatter per language, pinned to a version, enforced at save, commit and CI. The rules I apply across my JavaScript, TypeScript and Python repos, and why I stopped tuning the options.May 2025AI Made Building Faster. We Still Have to Decide What to Build.Small teams can ship more with AI. But unclear requirements still send us back to rebuild. My thoughts on planning, prototypes, and engineering judgment.Sep 2026
Continue exploringWriting indexAll writing.