Fix @tailwindcss/oxide Native Binding Error in pnpm Workspace with Next.js

4 min read.
Tags: nextjsjavascript

You're running Tailwind CSS v4 in a pnpm workspace with Next.js, and you get this:

Error: Cannot find native binding.
Caused by: Error: Cannot find module '@tailwindcss/oxide-darwin-arm64'
Caused by: Error: Cannot find module './tailwindcss-oxide.darwin-arm64.node'

Here's what's going on and how to fix it.

Why @tailwindcss/oxide can't find its native binding

Tailwind CSS v4 uses @tailwindcss/oxide, a Rust-based engine, for processing. It ships as a platform-agnostic wrapper, with the actual binary in optional packages like @tailwindcss/oxide-darwin-arm64, @tailwindcss/oxide-linux-x64-gnu, etc.

At runtime, oxide/index.js tries to load the native binary two ways:

// tries relative file first
require('./tailwindcss-oxide.darwin-arm64.node')

// falls back to the optional package
require('@tailwindcss/oxide-darwin-arm64')

In a standard npm or yarn setup this works fine. In a pnpm workspace with Next.js Turbopack, both fail.

The pnpm + Turbopack problem

pnpm uses an isolated linker: every package gets its own scoped node_modules with symlinks to its deps. So @tailwindcss/oxide-darwin-arm64 is symlinked under oxide's node_modules, and if you cd into the oxide package directory and run require('@tailwindcss/oxide-darwin-arm64'), it resolves correctly.

The issue is Turbopack. It compiles @tailwindcss/postcss and @tailwindcss/oxide into a separate worker bundle at .next/dev/build/postcss.js and runs it in an isolated worker context. That worker doesn't follow pnpm's symlink chain the same way, so both require paths fail: the package require and the relative file require.

Fix it

Step 1: make sure the optional dep is actually installed. pnpm sometimes skips it. Run with --force:

pnpm install --force

Step 2: copy the native .node binary directly into the oxide package directory. This is the key fix. The relative require ./tailwindcss-oxide.darwin-arm64.node works regardless of the module resolution context.

cp node_modules/.pnpm/@tailwindcss+oxide-darwin-arm64@4.3.0/node_modules/@tailwindcss/oxide-darwin-arm64/tailwindcss-oxide.darwin-arm64.node \
   node_modules/.pnpm/@tailwindcss+oxide@4.3.0/node_modules/@tailwindcss/oxide/

Adjust the version number (4.3.0) to match whatever you have installed.

Step 3: clear the Next.js cache. Turbopack caches the failing state, so even after the fix, the old error will replay until you delete it:

rm -rf apps/web/.next

Restart dev and it works.

Make it survive reinstalls

The copied binary gets wiped on every pnpm install. Add a postinstall script to your root package.json:

{
  "scripts": {
    "postinstall": "node scripts/fix-oxide-binding.mjs"
  }
}
// scripts/fix-oxide-binding.mjs
import { copyFileSync, existsSync, readdirSync } from 'node:fs'
import { join } from 'node:path'

const targets = {
  'darwin-arm64': 'darwin-arm64',
  'darwin-x64': 'darwin-x64',
  'linux-x64': 'linux-x64-gnu',
  'linux-arm64': 'linux-arm64-gnu',
}

const target = targets[`${process.platform}-${process.arch}`]
const store = 'node_modules/.pnpm'
if (!target || !existsSync(store)) process.exit(0)

const file = `tailwindcss-oxide.${target}.node`

for (const dir of readdirSync(store)) {
  const match = dir.match(/^@tailwindcss\+oxide@([^_]+)/)
  if (!match) continue
  const version = match[1]
  const src = join(store, `@tailwindcss+oxide-${target}@${version}`, 'node_modules/@tailwindcss', `oxide-${target}`, file)
  const dest = join(store, dir, 'node_modules/@tailwindcss/oxide', file)
  if (existsSync(src) && !existsSync(dest)) {
    copyFileSync(src, dest)
    console.log(`Copied ${file} into @tailwindcss/oxide@${version}`)
  }
}

It loops over every installed oxide version, so a lockfile with two Tailwind versions doesn't break your install. It covers darwin-arm64, darwin-x64, linux-x64, and linux-arm64. Add entries to targets if you need more.

TL;DR

pnpm isolates packages behind symlinks. Turbopack's PostCSS worker can't resolve optional native deps through those symlinks. Copying the .node binary directly into the oxide directory makes the relative require work, and clearing .next removes the cached failure. Add a postinstall script so the fix reapplies on every install.