Measured against Tailwind CSS 4.3.3 on 2026-09-19.
A tailwind.config.js sitting in your project root does nothing under Tailwind v4. Not "less than it used to". Nothing: we built a project with the file present and a project with the file deleted, and the two output stylesheets were byte-identical, down to the same SHA-256. The file only comes back to life when a @config line in your CSS points at it.
Once it does, most of it still works. We put all 19 keys of a realistic v3 config through one build each and diffed the CSS. Fourteen keys behave. Four are dropped without a single word on stderr. One is honored but quietly rewritten in a way that breaks every class in your markup.
And the documented list of what v4 cannot do is wrong. The docs name corePlugins, safelist and separator as unsupported.1 Two of those are correct. safelist works.
The setup
The probe is deliberately small, because the question is about the config loader and nothing else.
tailwindcss4.3.3,@tailwindcss/cli4.3.3,@tailwindcss/postcss4.3.3, Node 24.18.0.- One HTML file carrying every probe class.
- One CSS entry per config key:
@import "tailwindcss"plus a single@configline, so exactly one key is under test per build. A config that sets four things at once tells you nothing about which of the four moved the output. - The baseline is
@import "tailwindcss"with no@configand no config file anywhere on disk. It comes out at 53,006 bytes.
From there the verdict on a key is mechanical. If its probe reaches the output CSS, the key works. If the output is byte-identical to the baseline, the key did nothing at all.
That last test is stricter than eyeballing the file, and it is the one that makes the "silently ignored" rows trustworthy. corePlugins: { float: false } did not merely fail to remove float-left; it produced a stylesheet indistinguishable from one built with no config in the picture.
The table
| config key | under @config | evidence |
|---|---|---|
config file on disk, no @config line | nothing at all | byte-identical to a build with no config file present |
content | works, and adds to auto-detection | a class living only in a file outside the project appears; auto-detected classes all stay |
theme.extend.colors | works | #123456 in output |
theme.extend.spacing | works | 3.75rem in output |
theme.extend.screens | works | @media (width >= 1111px), and the new breakpoint extends .container too |
theme (full replace, not extend) | works | .bg-red-500 disappears |
theme.container center + padding | works | emits a second .container rule |
plugins | works | the plugin's addUtilities output is present |
presets | works | the preset's theme colour is present |
darkMode: 'class' | works | .dark\:underline:is(.dark *) replaces the media query |
important: true | works | !important count goes from 8 to 665 |
safelist, string entries | works, though the docs say otherwise | classes present in no source file are emitted |
safelist, { pattern: /re/ } entries | silently ignored | none of the covered classes are emitted, empty stderr |
blocklist | works | font-style: italic disappears |
prefix: 'tw-' | honored, then rewritten | warns, becomes the v4 tw: variant |
corePlugins | silently ignored | byte-identical to baseline |
separator: '_' | silently ignored | byte-identical to baseline |
future | no effect | byte-identical to baseline |
variants (a v2 key) | silently ignored | byte-identical to baseline |
The rest of this article is the rows that are not obvious.
The file on disk does nothing, and that is the whole migration trap
This is the row that costs people an afternoon, because the failure has no symptom. You upgrade, your config file is still sitting there, your build still succeeds, and your custom colours are simply gone. Nothing warns you. There is no "unused config file" notice, because from v4's point of view there is no config file. It is an unreferenced .js file in your repo like any other.
The fix is one line at the top of your CSS:
@import "tailwindcss";
@config "../../tailwind.config.js";cssThe path is resolved relative to the CSS file, not to the project root. And if you get it wrong, that at least is loud: a missing target is a hard build error with a non-zero exit, not a warning.
Error: Can't resolve '../configs/nope.js' in '.../lab/src'
Two @config lines in one stylesheet is legal, by the way. Both configs load, both themes merge, exit 0, no warning. That is convenient for a monorepo and a trap for anyone who forgets the second one is there.
safelist works, and the docs say it does not
The v4 @config documentation names exactly three keys as unsupported: corePlugins, safelist and separator.1 The safelist entry points you to @source inline() instead.2 Search for the question and the same three-item list comes back in every write-up on the first page of results.
Two of the three hold up. safelist does not. On 4.3.3:
// tailwind.config.js
module.exports = {
safelist: ['skew-y-12', 'bg-fuchsia-700'],
}jsBoth classes appear in the output. Neither appears in any source file the build scanned, so there is no other route by which they could have been generated. The same result comes out of the PostCSS plugin, so this is the engine's behaviour and not a quirk of the CLI.
What is actually broken is the form the docs never mention, the v3 object entry:
safelist: [
{ pattern: /^text-(emerald|sky)-(400|600)$/ },
{ pattern: /^grid-cols-(7|11)$/, variants: ['md'] },
]jsNot one of the six classes those two patterns cover was emitted. Exit code 0, empty stderr, no diagnostic of any kind. If you are carrying a v3 config whose safelist is patterns, and you read the docs, you would conclude your whole safelist is dead and go rewrite it. Half of it is fine. The pattern half is the part that vanished, and it vanished without telling you.
This is the sharper and more useful sentence: string entries survive, pattern entries do not. We would still move a pattern safelist to @source inline() with its brace expansion,2 because that is the path the project intends to support and a behaviour the docs disown could change in a patch release. But knowing which half broke is the difference between a targeted fix and a rewrite.
theme.container is not gone either
The upgrade guide says the container utility's center and padding options "no longer exist" in v4.3 We have quoted that line ourselves, in our Tailwind v3 to v4 migration write-up.
Under @config they exist. This config:
theme: {
container: { center: true, padding: '2.5rem' },
}jsproduces a second .container rule appended after the generated breakpoint ladder:
.container {
width: 100%;
@media (width >= 40rem) { max-width: 40rem; }
/* ... */
}
.container {
margin-inline: auto;
padding-inline: 2.5rem;
}cssWhich reframes what the upgrade tool is doing. npx @tailwindcss/upgrade rewrites a v3 theme.container block into an @utility container rule in your CSS, keeping your real values. That is not the tool compensating for a capability v4 lost. It is the tool moving you off @config and onto the CSS-first API, because @config is a compatibility shim and the project would rather you did not live in it.1
prefix is honored, and that is worse than being ignored
prefix is the one key where doing the work is more destructive than dropping it.
module.exports = { prefix: 'tw-' }jsThe build warns:
The prefix "tw" is invalid. Prefixes must be lowercase ASCII letters (a-z)
only and is written as a variant before all utilities. We have fixed up the
prefix for you. Remove the trailing `-` to silence this warning.
Then it applies the prefix, in the v4 syntax. In v3 a prefix was a string glued to the front of the class name, so you wrote tw-underline. In v4 the prefix is a variant, so it is tw:underline.3 The config is honored; it is your markup that is now wrong.
The output makes this concrete. tw:underline is emitted. tw-underline is not. And the plain underline in the same HTML is no longer emitted either, because with a prefix set every utility has to carry it.
So a v3 project whose classes are all tw--prefixed, migrated by adding one @config line, builds clean with a single yellow warning and renders completely unstyled. The stylesheet in our probe fell from 53,006 bytes to 4,713. That is the shape of this bug in production: not a build failure, a blank-looking page.
content adds sources, it does not replace them
In v3, content was the entire list of files Tailwind looked at. In v4, source detection is automatic, and the question is what an explicit content array does to that.
It adds. We put a class in a file outside the project directory and listed only that file in content. The class appeared, and every class found by automatic detection was still there. The config's content behaves like an @source line, not like a replacement for the scanner.
That is the friendly outcome, and it is worth knowing because the alternative would be catastrophic and silent: a narrow v3 content array that replaced auto-detection would strip most of your CSS. It does not.
CSS beats JavaScript, in both directions
If a token is defined in both places, @theme in your CSS wins:
@import "tailwindcss";
@config "../tailwind.config.js"; /* theme.extend.colors.probe = #123456 */
@theme {
--color-probe: #999999;
}css#999999 is in the output. #123456 is not present anywhere in the file.
Source order does not change it. Putting the @theme block above the @config line gives the same answer. This is not a cascade, where the last declaration wins; the CSS-first API that v4 was built around is simply authoritative over the compatibility shim.4 Useful when you are migrating a large theme one token at a time, since you can move a colour into @theme and leave the old JavaScript entry in place without them fighting.
The four silent keys
corePlugins, separator, variants and the safelist pattern form all produced output byte-identical to the baseline, with an empty stderr in every case. Only prefix printed a diagnostic across the entire matrix.
Three of them deserve a sentence each:
corePluginswas how you disabled a utility group in v3.{ float: false }leftfloat: leftin the output. There is no v4 equivalent. That is the answer, not a workaround waiting to be found.separatorwas the character between a variant and a utility. v4 hardcodes:.variantshas been dead since v3 made all variants available by default. It is in this table only because real configs still carry it.
And one that is not a bug at all: future: { hoverOnlyWhenSupported: true } produced a byte-identical build because v4 already emits @media (hover: hover) around hover: utilities unconditionally. The flag is not ignored so much as redundant. The future block in a v3 config is generally a list of things that became the default, which is why it is safe to delete rather than port.
What this site does
This blog runs Tailwind v4 with no JavaScript config at all. The theme is an @theme block, dark mode is handled in CSS, and there is no @config line to point anywhere. That is not a recommendation earned by suffering; the site was built after v4 shipped, so it never had a config to migrate.
The measurement above is the useful half. If you have a working tailwind.config.js and a deadline, @config will get you building today and the table tells you which four keys will lie to you on the way. Then move tokens into @theme at whatever pace you like, knowing the CSS side wins every collision.
Where this is weak
- One version. Everything here is 4.3.3. The
safelistresult in particular is behaviour the docs disown, so it is exactly the kind of thing that can change in a patch release without a changelog entry. Re-run the probe on your own version before betting on it. - CLI and PostCSS only. Not tested through
@tailwindcss/vite. All three load the same engine package, and CLI and PostCSS agreed on every probe, so we would be surprised by a difference, but we did not measure it. - Reaching the output is not the same as full fidelity. These probes ask whether a key affects the generated CSS. They do not verify that every nuance of a key's v3 semantics survived.
theme.extend.screensworks, for instance, but we checked that the breakpoint generates, not that every interaction between custom screens and the default set matches v3 exactly. - A realistic config, not an exhaustive one. Nineteen keys covers what sits in real v3 projects. It is not every key the v3 schema accepted.
Doing this on your own project
- Add
@config "path/to/tailwind.config.js";under your@import "tailwindcss";. The path is relative to the CSS file. - Build, and read stderr. A warning here is almost certainly
prefix. - If your markup uses a prefix, stop and deal with it first. Every class needs to change from
tw-foototw:foo, and until it does your build succeeds while your pages render unstyled. - Grep your config for
corePlugins,separator,variants, andsafelistentries that are objects rather than strings. Those four are gone and will not tell you. - Diff the output stylesheet against your v3 build. Byte counts are a blunt instrument but a large unexplained drop means something did not load.
- Migrate tokens into
@themegradually. CSS wins collisions, so you can move one colour at a time with the old entry still in the config.
FAQ
Does Tailwind v4 still read tailwind.config.js?
Not automatically. A tailwind.config.js in your project root is ignored entirely; we verified the output is byte-identical to a build with no config file present. It is loaded only when a @config "path/to/tailwind.config.js"; line in your CSS points at it.1
What does the @config directive do in Tailwind v4?
It loads a legacy v3 JavaScript config file. The docs describe it as existing "solely for compatibility with Tailwind CSS v3.x".1 The path resolves relative to the CSS file containing the directive, and a path that does not resolve is a hard build error rather than a warning.
Which tailwind.config.js options do not work in v4?
Measured on 4.3.3: corePlugins, separator, variants, and safelist entries written as { pattern: /re/ } objects. All four are dropped with no warning. future has no effect either, but only because its flags became defaults. The documented list of unsupported keys is corePlugins, safelist and separator;1 our measurement agrees on two of the three and finds that string safelist entries do work.
Does safelist work in Tailwind v4?
String entries do, on 4.3.3, through both the CLI and the PostCSS plugin, despite the documentation listing safelist as unsupported.1 Regex { pattern } entries do not and fail silently. The supported path is @source inline() in your CSS, which also handles brace expansion for generating class variations.2
What happened to theme.container's center and padding in v4?
The upgrade guide says they no longer exist.3 Loaded through @config they do work, emitting a .container rule with margin-inline: auto and your padding-inline value. The CSS-first replacement is an @utility container rule, which is what npx @tailwindcss/upgrade writes for you.3
Does prefix still work in Tailwind v4?
Yes, with a syntax change that will break your markup. v4 prefixes are variants, so tw-underline becomes tw:underline.3 A v3 config's prefix: 'tw-' is accepted with a warning about the trailing hyphen, then applied in the new form, which means a prefixed v3 codebase builds successfully and renders unstyled.
Does @theme override a JavaScript config theme?
Yes, and source order does not matter. A token defined in both an @theme block and the config's theme resolves to the @theme value, with the JavaScript value absent from the output. That makes an incremental migration safe: move tokens to CSS one at a time and leave the config entries in place.
Disclosure
The migration article linked above was written about selim.services, one of our own sites. The measurements in this article were taken on a throwaway project built for the purpose, not on any property of ours.
Sources
Checked 2026-09-19.
Sources
-
Tailwind CSS functions and directives: the @config directive - that
@configloads a legacy JavaScript-based configuration file, that it exists solely for compatibility with Tailwind CSS v3.x, and that thecorePlugins,safelistandseparatoroptions from a JavaScript config are not supported in v4. -
Tailwind CSS: safelisting specific utilities - that
@source inline()is the v4 mechanism for forcing Tailwind to generate classes that appear in no source file, and that it supports brace expansion for generating variations of a class. -
Tailwind CSS upgrade guide - that
theme.container'scenterandpaddingoptions no longer exist and are replaced by an@utility containerrule, and that theprefixoption is now written as a variant before the utility name rather than as a string prepended to it. -
Tailwind CSS v4.0 announcement - that v4 replaces the JavaScript configuration file with a CSS-first configuration model in which the theme is declared with
@themeand exposed as CSS variables.