The npm package tailwindcss-form-styles did something clever. Its src/index.js re-exported the genuine Tailwind forms plugin, so if you installed it and added it to your config, your forms styled correctly and nothing looked wrong. Appended to that working cover was an obfuscated blob ending in eval(atob(...)) over roughly 41KB of base64. Its advisory notes the payload "fires as soon as any Tailwind build loads this plugin."
It was one of ten fake Tailwind plugins published to npm since October 2025. Tailwind is in almost every frontend that Bolt, Lovable, v0 and Cursor generate, and founders add plugins to it by asking the assistant for one.
TL;DR
At least ten typosquats of Tailwind form-plugin names hit npm between October 2025 and September 2026, seven of them in the last four weeks. None of the detailed advisories describes an install script. They execute when Node requires the module or when Tailwind loads the plugin from your config, so npm install --ignore-scripts does not catch them. Search your lockfile for the names below, and check every entry in your Tailwind plugins array against the package it claims to be.
The names
Every date below comes from the npm registry and the OSV advisories, read on 22 September 2026.
| Package | Advisory | On npm | Removed by |
|---|---|---|---|
tailwindcss-forms | MAL-2025-191595 | 2025-10-29 | unpublish |
tailwindcss-forms-kit | MAL-2026-693 | 2025-12-26 to 2026-02-03 | security placeholder |
tailwindcss-form-bundler | MAL-2026-1209 | 2026-02-12 to 2026-03-03 | security placeholder |
tailwind-extension-kit | MAL-2026-14189 | 2026-08-17 | unpublish |
tailwindcss-form-styles | MAL-2026-14568 | 2026-08-26 to 08-27 | unpublish |
tailwind-forms-styles | MAL-2026-16195 | 2026-09-07 to 09-15 | security placeholder |
tailwind-form-kit | MAL-2026-16139 | 2026-09-08 to 09-11 | security placeholder |
tailwindcss-form-ui | MAL-2026-16262 | 2026-09-17 | unpublish |
tailwindcss-form-utils | MAL-2026-16263 | 2026-09-17 | unpublish |
tailwindcss-forms-ui | MAL-2026-16295 | 2026-09-17 to 09-18 | unpublish |
Read that list slowly, because the confusability is the attack. tailwindcss-form-styles and tailwind-forms-styles are two different malicious packages, published three weeks apart, differing by two characters. So are tailwindcss-form-ui and tailwindcss-forms-ui. The real package is @tailwindcss/forms.
Names also rotate fast. tailwindcss-form-ui came off npm at 14:44 UTC on 17 September, and tailwindcss-form-utils went up at 14:54, ten minutes later. tailwindcss-forms-ui followed the same night.
None of these needed an install script
Most npm supply-chain advice assumes the payload fires during installation. That is where npm install --ignore-scripts earns its reputation.
Not one of the seven advisories with technical detail mentions postinstall or preinstall. Every one describes execution at require time or plugin-load time. MAL-2026-14189 is the most explicit:
Following the README's instruction to register the default export as a Tailwind plugin causes Tailwind to invoke
getPlugin, executing attacker-controlled JavaScript with full Node.js capabilities on the developer's machine.
And MAL-2026-14568, on the working-cover package:
The eval fires as soon as any Tailwind build loads this plugin, resulting in attacker-controlled code execution in the installer's build environment (developer machines and CI runners).
So the sequence is: you install a package, which does nothing. You add it to tailwind.config.js, exactly as its README says. That is the step that runs the code, on your machine and on every CI runner that builds the project.
--ignore-scripts does not help against this cluster. It blocks lifecycle hooks, and it is still worth using, because plenty of npm attacks do use them. It cannot block code that runs because your own build tool imported the module and called it. Registering a plugin is calling it.
We already track three execution triggers across the 2026 npm compromises: install hooks, build-time execution through node-gyp, and import-time payloads. Plugin registration is a fourth, and it is the one most likely to reach a founder, because editing a config file feels like configuration rather than like running something.
What actually links them
The advisories publish SHA-256 hashes of the files they sampled. Cross-referencing those hashes across the seven 2026 records shows two payloads that are byte-identical across multiple packages:
f5fd0ca9d764... tailwind-forms-styles, tailwindcss-form-ui, tailwindcss-form-utils
fd1ef3edfc6c... tailwind-form-kit, tailwindcss-forms-ui
Identical files, not similar ones. MAL-2026-14568 goes further and notes an embedded campaign tag string, A10-npm_new2. Four of the advisories also record that the package's own package.json pointed its repository field at tailwindlabs/tailwindcss-forms, borrowing the real project's credibility, and one mirrored its README verbatim.
tailwind-extension-kit is the odd one out and probably a different actor. It shares no file hashes with the others, used a hardcoded IP endpoint rather than the blockchain dead-drop the rest used, and executed through new Function rather than child_process. It has a Tailwind-themed name and nothing else in common.
The September packages retrieved their real payload from the Ethereum blockchain, polling RPC endpoints for transactions from a hardcoded sender address. You can seize a domain; you cannot take down an Ethereum address. We are not printing the address, because the advisories give two different transcriptions of it that are character transpositions of each other, and we have no way to tell which is correct.
What we do not know, stated plainly
Download counts for these packages, from 1 August to 22 September 2026, were small:
| Package | Downloads |
|---|---|
tailwind-form-kit | 893 |
tailwindcss-form-styles | 615 |
tailwindcss-forms-ui | 180 |
tailwind-forms-styles | 78 |
tailwindcss-form-ui | 0 |
tailwindcss-form-utils | 0 |
For comparison, @tailwindcss/forms does about 3.3 million a week. The two packages that lived roughly an hour got zero downloads between them, and a meaningful share of the rest is likely security vendors pulling samples rather than anyone's actual project. Nobody was mass-compromised here.
So this is not a "you probably have this installed" story. It is a "the next one of these is coming and your main defence does not apply" story.
Two other limits worth knowing. Once npm removes a package, the tarballs are gone, so nobody can go back and audit which versions were clean. And two of the advisories list their affected range as everything from version 0 onward, so a scanner will flag every version of those packages, including the first. Do not read a version's absence from an advisory as evidence it was safe; the version lists record which files got sampled.
How to check, in about two minutes
grep -nE "tailwindcss-forms-kit|tailwindcss-form-bundler|tailwind-extension-kit|tailwindcss-form-styles|tailwind-forms-styles|tailwind-form-kit|tailwindcss-form-ui|tailwindcss-form-utils|tailwindcss-forms-ui" \
package.json package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null
That covers nine of the ten. The tenth, tailwindcss-forms, is left out on purpose: as a bare string it also matches tailwindlabs/tailwindcss-forms, the legitimate repository path that shows up in plenty of innocent lockfiles, so searching for it mostly produces false alarms.
No output means none of them reached your dependency tree, which is the likely outcome. If something matches, rotate every credential that machine could reach and do it from a different machine, because the advisories say to treat the host as fully compromised. Deleting the package does not un-steal anything already sent.
Then read your Tailwind config against what each package really is:
grep -n -A15 "plugins" tailwind.config.js
The useful heuristic is scope. Tailwind Labs publishes its own plugins under @tailwindcss, so the real forms plugin is @tailwindcss/forms, currently 0.5.11, with its repository at tailwindlabs/tailwindcss-forms. An unscoped package whose name is a longer, more descriptive-sounding version of a first-party one is the exact shape this cluster used. tailwind-forms-styles reads like a better name than @tailwindcss/forms, and that is the trick.
Unscoped does not mean fake. tailwind-scrollbar is real and unscoped. tailwindcss-animate is real and unscoped too, though its last release was in 2023 and it has no repository field, so much of the Tailwind v4 ecosystem has moved on. Check the repository link and the download history, not the shape of the name.
Two things this is not
If you search for Tailwind and blockchain malware you will hit a March 2026 story about attackers force-pushing commits that modified tailwind.config.js and .gitignore in compromised repositories. That one used Tron and Aptos, involved no npm packages, and is unrelated to this cluster despite sharing all three keywords.
You will also hit material-tailwindcss, a typosquat from September 2022 that did use a postinstall hook. Also unrelated, and a good illustration of why the install-script assumption became the default.
Why this keeps landing on vibe-coded projects
Typosquatting is not new. What changed is who picks the package name.
Ask an assistant to add form styling to a Tailwind project and you get a name in a code block plus an instruction to register it in your config. You cannot tell a real plugin from a fake one by reading the name, because the fake was designed to read as more legitimate than the real one. Then you follow the README, and on these packages that was the moment the code ran.
The defence is dull and it works: check the name against the registry before installing, prefer the scoped first-party package when one exists, and keep a lockfile so you know exactly what landed.
Is tailwind-forms-styles safe?
No. It impersonates the real @tailwindcss/forms plugin, and npm replaced it with an empty 0.0.1-security package on 15 September 2026. Note the name carefully: tailwind-forms-styles and tailwindcss-form-styles are two different malicious packages from the same period. If either is in your lockfile, treat the machine that installed it as compromised.
Does npm install --ignore-scripts protect me from these?
Not from this cluster. None of the seven detailed advisories describes a postinstall or preinstall hook. Every one describes code running at require time or when Tailwind loads the plugin from your config. --ignore-scripts is a real precaution against lifecycle-script attacks, and it does nothing here, because your own build tool is what executes the code.
How do I know if I installed a fake Tailwind plugin?
Search your lockfile for the known names. Then open tailwind.config.js and check each entry in the plugins array against the package it claims to be. Tailwind Labs publishes its own plugins under the @tailwindcss scope, so the real forms plugin is @tailwindcss/forms. An unscoped package with a longer, more descriptive-sounding name did not come from them.
Would I notice if one of these was installed?
Probably not. tailwindcss-form-styles re-exported the genuine tailwindcss-forms plugin as a working cover, so your form styling would render correctly while the appended payload ran on every build. There is no broken page to tip you off. That is the point of the design.
Is this a vulnerability in Tailwind CSS?
No. Tailwind Labs is the impersonated party here, not the source of the problem. These packages put tailwindlabs/tailwindcss-forms in their own repository field to borrow its credibility. A Tailwind plugin is ordinary JavaScript that Tailwind calls during a build, which is documented behaviour and exactly what the packages abused.
Check What Your App Actually Installed
Scan your project for exposed credentials and risky dependencies.