What is Knip?
Knip helps declutter JavaScript and TypeScript projects by detecting dead exports, unused files, and dependencies. It does this intelligently using a plugin system modeled after real-world tooling and frameworks. This fine-grained approach results in an accurate representation of dead code.
To get started with Knip, run:
pnpm create @knip/config
Add a script to your package.json
:
{ "scripts": { "knip": "knip" }}
Or just run it directly:
pnpx knip
Out of the box, Knip technically works with Tailwind CSS v4, but not perfectly. Its lack of support for Tailwind’s new @plugin and @import directives can cause false positives.
What changed in Tailwind CSS v4?
Tailwind CSS v4 introduced a new configuration approach that is CSS-first, which allows for imports and plugins. However, Knip doesn’t scan these directives by default, which leads to false positives. For example, if you have a Tailwind CSS file like this:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui";
@plugin "@iconify/tailwind4";
When you run knip
, you might see output like this:
Unused devDependencies (4)@iconify/tailwind4 package.json:29:6@tailwindcss/typography package.json:32:6daisyui package.json:36:6tailwindcss package.json:45:6error: script "knip" exited with code 1
One quick fix is to ignore these dependencies in your Knip configuration file:
import type { KnipConfig } from "knip";
export default { ignoreDependencies: [ "@iconify/tailwind4", "@tailwindcss/typography", "daisyui", "tailwindcss", ],} satisfies KnipConfig;
We use satisfies KnipConfig
to ensure our config matches the expected structure. It’s a TypeScript safety check, not required for runtime.
Knip supports a custom compilers configuration option, and with a bit of regex magic, we can teach it to understand Tailwind’s directives like @import and @plugin.
How to configure Knip for Tailwind CSS v4?
import type { KnipConfig } from "knip";
export default { compilers: { css: (text: string) => { // Converts @import and @plugin directives into JS-style imports return [...text.matchAll(/@(?:import|plugin)\s+["']([^"']+)["']/g)] .map(([_, dep]) => `import "${dep}";`) .join("\n"); }, },} satisfies KnipConfig;
And if you’ve modified knip
’s project
field, be sure to include .css
files as well:
import type { KnipConfig } from "knip";
export default { project: "**/*.{ts,tsx,css}",} satisfies KnipConfig;
This prevents false positives, keeps your reports clean, and avoids accidentally removing real dependencies.
How does this work?
This configuration defines a custom compiler for CSS files. It looks for @import and @plugin directives using a regular expression and transforms them into JavaScript-style import statements that Knip can understand and analyze like regular module imports.
This borrows from Knip’s own css compiler example.
matchAll is used to find all matches of a regular expression in a string.
The regex pattern @(?:import|plugin)\s+["']([^"']+)["']
breaks down as:
@(?:import|plugin)
- Matches either @import or @plugin\s+
- Matches one or more whitespace characters["']([^"']+)["']
- Captures the dependency name inside quotes
In the earlier example, it would return:
[ ['@import "tailwindcss"', "tailwindcss"], ['@plugin "@tailwindcss/typography"', "@tailwindcss/typography"], ['@plugin "daisyui"', "daisyui"], ['@plugin "@iconify/tailwind4"', "@iconify/tailwind4"],];
You can further learn about regex at regex101.
Then map
transforms each match into an import statement using the captured dependency name, and the join("\n")
combines them into a single string to make it look like valid JavaScript/TypeScript code.
Once that’s in place, running knip
will no longer flag these dependencies:
✂️ Excellent, Knip found no issues.
Tailwind’s new config is great, but Knip doesn’t know what to do with @plugin or @import by default. This setup bridges that gap and keeps your reports clean.