How to Fix: Turbopack fails to pack css.modules with utf8-bom encoding

6 min read

Turbopack breaks on CSS Modules saved with UTF-8 BOM because the parser sees hidden bytes before the first valid token.

In affected Next.js dev setups, running the app with –turbo can fail when a .module.css file is encoded as UTF-8 with BOM. The page may not render, styles may fail to load, or Turbopack may report a CSS parsing or module processing error even though the stylesheet looks correct in the editor. The underlying issue is not your CSS syntax; it is the file encoding.

Reproducing the issue

The issue described in the reproduction repository appears when Turbopack processes a CSS Module containing a leading byte order mark. A typical reproduction flow is:

  1. Clone the reproduction app.
  2. Install dependencies.
  3. Start Next.js in dev mode with Turbopack.
  4. Open the page that imports the BOM-encoded CSS Module.
git clone https://github.com/Martinii89/nextjs-turbo-bom-bug.git
cd nextjs-turbo-bom-bug
npm install
npm run dev -- --turbo

If the referenced CSS Module was saved as UTF-8 BOM, Turbopack can choke on the file while the same project may behave differently under the classic webpack-based dev pipeline.

Understanding the Root Cause

This happens because a UTF-8 BOM adds invisible bytes at the very beginning of the file: EF BB BF. Some parsers strip that marker automatically before tokenizing content. Others do not handle it consistently in every code path, especially in newer or faster bundling pipelines.

With Turbopack, the CSS Module pipeline expects the first bytes to belong to valid CSS input. When the BOM is preserved instead of normalized away, the parser may interpret the file as containing an unexpected leading character. That can break one of several internal steps:

  • CSS tokenization fails before selectors are read.
  • CSS Module transformation fails before class name export mapping is generated.
  • Hot reload state becomes inconsistent because the module never compiles correctly.

The reason this feels confusing is that editors usually hide BOM characters. The stylesheet looks perfectly valid, but its first bytes are not what the bundler expects. In short, the bug is caused by an encoding mismatch, not a styling mistake.

Step-by-Step Solution

The most reliable fix is to convert every affected .module.css file from UTF-8 with BOM to UTF-8 without BOM.

1. Identify BOM-encoded CSS Module files

On macOS or Linux, you can inspect the first bytes of a file:

xxd -g 1 -l 3 app/page.module.css

If the output starts with ef bb bf, the file contains a BOM.

2. Convert the file to UTF-8 without BOM

One quick Node.js approach is to rewrite the file without the BOM marker:

node -e "const fs=require('fs'); const p='app/page.module.css'; let s=fs.readFileSync(p,'utf8'); if (s.charCodeAt(0)===0xFEFF) s=s.slice(1); fs.writeFileSync(p,s,'utf8'); console.log('BOM removed from', p);"

You can also use a shell command on Unix-like systems:

sed -i '1s/^\xEF\xBB\xBF//' app/page.module.css

3. Restart Turbopack

After rewriting the file, stop the dev server and start it again:

npm run dev -- --turbo

This ensures Turbopack re-reads the corrected file from disk and rebuilds the module graph cleanly.

4. Prevent the issue from returning

Configure your editor to save files as UTF-8 without BOM. In many editors this is listed as UTF-8 or UTF-8 no BOM.

For teams, add a lightweight validation script to catch BOM markers before commit:

const fs = require('fs');
const path = require('path');

function walk(dir, results = []) {
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory() && !['node_modules', '.next', '.git'].includes(entry.name)) {
      walk(full, results);
    } else if (entry.isFile() && full.endsWith('.module.css')) {
      results.push(full);
    }
  }
  return results;
}

const files = walk(process.cwd());
const bad = [];
for (const file of files) {
  const buf = fs.readFileSync(file);
  if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
    bad.push(file);
  }
}

if (bad.length) {
  console.error('Found UTF-8 BOM in CSS Modules:');
  for (const file of bad) console.error('-', file);
  process.exit(1);
}

console.log('No BOM found in CSS Modules.');

Save it as scripts/check-css-bom.js and run it with:

node scripts/check-css-bom.js

5. Optional: auto-fix all CSS Modules in the repo

const fs = require('fs');
const path = require('path');

function walk(dir, results = []) {
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory() && !['node_modules', '.next', '.git'].includes(entry.name)) {
      walk(full, results);
    } else if (entry.isFile() && full.endsWith('.module.css')) {
      results.push(full);
    }
  }
  return results;
}

for (const file of walk(process.cwd())) {
  let text = fs.readFileSync(file, 'utf8');
  if (text.charCodeAt(0) === 0xFEFF) {
    fs.writeFileSync(file, text.slice(1), 'utf8');
    console.log('Fixed', file);
  }
}

This workaround is safe because it removes only the hidden BOM character at the start of the file.

Common Edge Cases

  • Other file types may also be affected. While this issue targets css.modules, BOM markers in global CSS, JSON, config files, or source code can trigger similar parser failures depending on the toolchain.
  • The editor reintroduces BOM on save. If the problem comes back after every edit, your IDE or a file conversion plugin is saving files with BOM automatically.
  • Git does not normalize encodings. Git tracks bytes, not text semantics. A BOM can move between machines unchanged unless you validate files in CI or pre-commit hooks.
  • The error persists due to cache state. If Turbopack still behaves oddly after fixing the file, stop the server, remove .next, and restart.
rm -rf .next
npm run dev -- --turbo
  • Multiple files are affected. One fixed file may reveal another BOM-encoded CSS Module elsewhere in the project.
  • Cross-platform shell commands vary. The sed -i syntax differs between GNU sed and BSD sed, so the Node.js script is often the most portable option.
  • FAQ

    Why does this fail in Turbopack but sometimes not in webpack?

    Different bundlers and parser stacks normalize file input differently. Webpack or a loader in its pipeline may strip BOM characters earlier, while Turbopack may currently pass the raw bytes into a CSS Module parsing path that is less tolerant.

    Is the CSS syntax actually invalid?

    No. The stylesheet content is usually valid. The failure is caused by the hidden BOM bytes appearing before the first CSS token, which makes the parser think the file starts with unexpected input.

    What is the best long-term fix for teams?

    Use UTF-8 without BOM as a repository standard, configure editors accordingly, and add an automated check in CI or a pre-commit hook so BOM-encoded files never reach the Turbopack pipeline.

    Until the upstream bug is fully addressed, removing the UTF-8 BOM from affected CSS Module files is the fastest and most reliable way to get Next.js Turbopack development builds working again.

    Leave a Reply

    Your email address will not be published. Required fields are marked *