Skip to content

The midcode package

Build midcode.css without the app, in your production build, in CI or beside an agent, with the midcode CLI, its Vite plugin or its Next.js wrapper.

View as Markdown

In a project without Tailwind 4, your styles are mid: classes in the code, and midcode.css is their CSS. The app rewrites that file while the project is open in it. The midcode package does the same job everywhere else: a command line tool, a Vite plugin and a Next.js wrapper that build midcode.css from the classes in your code.

A project that uses Tailwind 4 doesn’t need it: there, midcode writes Tailwind’s own classes and Tailwind compiles them.

When you need it

midcode.css is a plain file in your repository, so a site that was only ever edited in midcode builds and deploys with nothing added. Add the package when the classes can change while the app isn’t looking:

  • You or an agent write mid: classes by hand, in an editor or a terminal.

  • You want the file rebuilt in CI or in your production build, so that it can never be out of date.

  • You’d rather not commit a generated file, and build it instead. Then always pass --out, so that a fresh checkout knows where the file goes.

  • A teammate works on the project without midcode.

Install

Terminal
npm install -D midcode
pnpm add -D midcode
yarn add -D midcode

It needs Node 20 or later. Its one dependency is tailwindcss (version 4), which it uses as the compiler for mid: classes. Your own stylesheets don’t go through Tailwind and nothing in your build config changes unless you add one of the integrations below.

Any project: the command

Run it before your build, or leave it watching while you work:

package.json
{
  "scripts": {
    "prebuild": "midcode",
    "dev:css": "midcode --watch"
  }
}
CommandWhat it does
midcodeBuilds midcode.css once.
midcode --watchBuilds it, and again whenever your code or your theme changes. -w for short.
--root <dir>The project’s folder. Default: the current one.
--out <file>Where midcode.css goes when the project has none yet.
--helpPrints the options. -h for short.

A build prints one line:

Terminal
$ npx midcode
midcode: 42 classes → /Users/you/site/app/midcode.css

It adds “(up to date)” when the file didn’t need to change. On an error it prints midcode: <message> and exits with code 1, so a prebuild script stops the build.

Where your package manager doesn’t run pre scripts by itself (Yarn 2 and later), call it in the script: "build": "midcode && vite build".

Vite, Astro, SvelteKit, React Router, Remix

Add the plugin and midcode.css is built when the dev server or a build starts, and again whenever a source file or the theme is added, changed or removed while the dev server runs.

vite.config.ts
import { defineConfig } from 'vite'
import midcode from 'midcode/vite'

export default defineConfig({ plugins: [midcode()] })

The project’s folder is Vite’s own root. Pass midcode({ root, out }) to change it, with the same meaning as the command’s options.

Next.js

Wrap your config. midcode.css is built before Next starts, and kept up to date while next dev runs.

next.config.ts
import withMidcode from 'midcode/next'

export default withMidcode({
  // your config
})

withMidcode takes your config as an object or as a function, and options as a second argument: withMidcode(config, { root, out }). If the build of midcode.css fails, it prints the error and lets Next go on.

From code

The same two functions the command uses are exported:

TypeScript
import { build, watch } from 'midcode'

const result = await build({ root: process.cwd() })
// { file: '/abs/path/midcode.css', classes: 42, changed: true }

const stop = await watch({}, (r) => {
  if (r instanceof Error) console.error(r.message)
})

Starting without the app

The package always writes to the midcode.css the project already has, wherever it is. In a project that has none, say where it goes and import it in your entry:

Terminal
npx midcode --out src/midcode.css
src/main.tsx
import './midcode.css'

Without --out, and with no midcode.css in the project, the command stops: “No midcode.css in /Users/you/site. Pass where it goes (--out src/midcode.css) and import it in your app’s entry.”

From then on, write classes with the prefix first and run the command:

HTML
<h1 class="title mid:text-[80px] mid:md:text-[56px] mid:hover:text-[#ff5b2e]">Hello</h1>

Tokens go in .midcode/theme.css. See Theme and design tokens for the format.

.midcode/theme.css
@theme {
  --color-brand: #ff5b2e;        /* mid:bg-brand, mid:text-brand */
  --font-display: "Fraunces";    /* mid:font-display */
}

@utility lead {                  /* mid:lead, mid:md:lead */
  font-size: 20px;
  line-height: 1.5;
}

When you open that project in midcode later, it reads the same classes and the same theme and carries on from there. Put the file where the app looks for it: next to your entry, or in src/, app/, src/app/, src/routes/ or the project’s root.

What it reads

  • Classes: every mid: class in the project’s .js, .jsx, .ts, .tsx, .mjs, .cjs, .svelte, .astro, .vue, .html, .md and .mdx files. It skips node_modules, .git, .next, .nuxt, .svelte-kit, .astro, .vercel, .turbo, .cache, dist, build, out and coverage.

  • The theme: .midcode/theme.css, when there is one.

  • The output: the first midcode.css it finds in the project. That file is rewritten whole, and only when its content would change.

What it writes is the same CSS the app writes: one rule per class, every declaration !important, in layers named mid-theme and mid-utilities, followed by the reset scoped to mid-base. The full description is in Without Tailwind.

Limits

  • The command and the integrations build the stylesheet. They don’t add the import to your entry and they don’t write classes: that’s the app, or you.

  • A class name put together at runtime ('mid:p-' + size) isn’t found. Write classes out whole.

  • Version 0.1.0 doesn’t read server templates (.php, .erb, .liquid, .twig and the like). The app reads them from its next release.

  • If the project has more than one midcode.css, only the first one found is written.