GrapesJS with Tailwind CSS v4: utilities in the canvas, compiled CSS on export
Run Tailwind's in-browser compiler inside the GrapesJS canvas so utility classes render while editing, then compile only the CSS a published page uses on the server. Tested with Tailwind 4.1.14 and GrapesJS 0.23.5, including class names with colons and slashes.
TL;DR
- Load @tailwindcss/browser into the canvas iframe with canvas.scripts: utility classes render while editing, and the script is not part of the exported HTML.
- Tailwind documents the in-browser build as development-only, so never ship it on published pages; compile real CSS from the exported HTML instead, which our test does and compares against the canvas.
- Class names like md:py-24, w-1/2 and hover:bg-blue-700 survived GrapesJS's parser and getHtml() unchanged in 0.23.5; the Style Manager, which writes its own CSS rules, is a separate problem this guide does not solve.
Two jobs, two tools
A Tailwind page needs CSS generated from the classes it uses. In an editor that has to happen twice:
- While editing, inside the canvas iframe, every time a class appears.
- When publishing, once, for exactly the classes on the page.
Tailwind's in-browser build (@tailwindcss/browser, the file behind its Play CDN) does the first
job well and is documented by Tailwind as "designed for development purposes only, and is not
intended for production." An editor canvas is a development surface; your published pages are not.
So the browser build goes into the canvas, and the published CSS is compiled on the server.
The editor
import grapesjs from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
const editor = grapesjs.init({
container: '#gjs',
height: '100vh',
storageManager: false,
canvas: {
// Tailwind's in-browser compiler, self-hosted (copied from @tailwindcss/browser).
// It runs inside the canvas iframe only and is not part of the exported HTML.
scripts: ['/vendor/tailwindcss-browser.js'],
},
projectData: {
pages: [
{
component: `
<section class="bg-slate-900 px-6 py-16 md:py-24 text-center">
<h1 class="text-5xl font-bold tracking-tight text-white">Ship pages faster</h1>
<p class="mx-auto mt-4 w-1/2 text-lg text-slate-300">Tailwind classes, edited visually.</p>
<a href="#" class="mt-8 inline-block rounded-md bg-blue-600 px-5 py-3 font-semibold text-white hover:bg-blue-700">Get started</a>
</section>`,
},
],
},
});
editor.Blocks.add('tw-card', {
label: 'Card',
category: 'Tailwind',
content: `<div class="max-w-sm rounded-xl bg-white p-6 shadow-lg">
<h3 class="text-xl font-semibold text-slate-900">Card title</h3>
<p class="mt-2 text-slate-600">Card body text.</p>
</div>`,
});
// Exposed for the test harness.
Object.assign(window, { editor });canvas.scripts appends scripts to the head of the canvas iframe only. GrapesJS's typings state
that these scripts are not printed in the export code, and our test confirms the exported HTML has
no reference to the compiler. Self-host the file (copy it from
node_modules/@tailwindcss/browser/dist/index.global.js) rather than pointing at a CDN, so the
editor keeps working under a strict Content Security Policy and does not depend on a third party.
npm install grapesjs@0.23.5 tailwindcss@4.1.14 @tailwindcss/browser@4.1.14Compiling CSS for the published page
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { compile } from 'tailwindcss';
const require = createRequire(import.meta.url);
/** Resolves `@import "tailwindcss"` (and its sub-imports) from node_modules. */
async function loadStylesheet(id, base) {
const file = id.startsWith('.') ? path.resolve(base, id) : require.resolve(id === 'tailwindcss' ? 'tailwindcss/index.css' : id);
return { path: file, base: path.dirname(file), content: await fs.readFile(file, 'utf8') };
}
/** Every token inside a class attribute. Tailwind ignores tokens that are not utilities. */
function classCandidates(html) {
const candidates = new Set();
for (const match of html.matchAll(/\sclass="([^"]*)"/g)) {
for (const token of match[1].split(/\s+/)) if (token) candidates.add(token);
}
return [...candidates];
}
/**
* Compiles the CSS a published page needs from the HTML GrapesJS exported: only the utilities
* that appear in it, plus Tailwind's preflight and theme variables.
*/
export async function buildTailwindCss(html, input = '@import "tailwindcss";') {
const compiler = await compile(input, { base: process.cwd(), loadStylesheet });
return compiler.build(classCandidates(html));
}classCandidates collects every token from class attributes. Tailwind discards tokens that are
not utilities, so over-collecting is harmless. The result contains preflight, the theme variables
and only the utilities present. In our test it was 9 kB for the example page, and it did not
contain grid-cols-12, which the page never uses.
A caveat: compile() is exported by the tailwindcss package with types, but Tailwind's
documentation covers its CLI, Vite and PostCSS integrations rather than this function. Pin the
version. The documented alternative is to write the exported HTML to a file and run the Tailwind
CLI with that file as a source.
What our test checks
The editor is bundled with esbuild and loaded in headless Chromium with the self-hosted browser build. The test asserts that:
- inside the canvas, the heading's computed font size is 48px (
text-5xl) and the button has a background colour (bg-blue-600); w-1/2makes the paragraph half its container's content width;- a block with classes not present on load, including
shadow-lg, gets a computed box shadow after it is added; md:py-24,w-1/2andhover:bg-blue-700appear unchanged ineditor.getHtml(), and the compiler script does not;- CSS compiled by
build-css.mjsfrom that HTML, loaded on a standalone page, gives the heading the same computed font size, colour, background and border radius as in the canvas, the button the same background, and the paragraph the samew-1/2ratio.
If you want pre-built Tailwind blocks
Hand-writing blocks is covered in custom blocks and component types.
The community grapesjs-tailwind plugin bundles the Tailblocks set; see the FAQ for what we found
in its package metadata. We have not tested it, nor the premium plugin below.
Limitations
- The Style Manager fights utility classes. GrapesJS's Style Manager writes CSS rules against
selectors; it does not add or remove Tailwind classes. Teams usually hide it or replace it with a
class editor. We did not test style editing on Tailwind classes, and the grapesjs-tailwind
plugin's README recommends a custom
selectorManager.escapeNamefor class names with slashes, which suggests selector handling is where problems appear. - Canvas performance. The in-browser compiler observes the canvas document. We did not measure its cost on large pages.
- Tailwind configuration. Custom theme values go in the CSS passed to
compile(), for example an@themeblock, and must also reach the canvas; keeping the two in sync is not covered. - Tailwind v3. Everything here is v4. The v3 Play CDN and configuration model differ.
For the bigger decision of whether GrapesJS fits a Tailwind-based product at all, the GrapesJS review and GrapesJS vs Puck cover the architecture, and the playground shows the default editing experience.
Frequently asked questions
- Should I use the grapesjs-tailwind plugin?
- We did not test it. Version 1.0.13 (MIT, published October 2025) describes itself as a work in progress, loads Tailwind from the Play CDN at cdn.tailwindcss.com by default, requires grapesjs-plugin-forms, and was developed against grapesjs ^0.22.13. Its value is the Tailblocks block set; evaluate it against your GrapesJS version before adopting it.
- Do new classes typed in the editor get styled?
- In the canvas, yes: the in-browser compiler watches the document. Our test drops a block with shadow-lg, which does not appear in the initial page, and waits until its computed box-shadow is set. On the published page, only if you recompile the CSS from the saved HTML.
- Is the tailwindcss compile() function a public API?
- It is exported by the tailwindcss package and its types ship with it, but Tailwind's documentation describes the CLI, Vite and PostCSS integrations, not compile(). Pin your Tailwind version if you use it, or write the exported HTML to disk and run the Tailwind CLI over it instead.
Sources
- Tailwind CSS documentation: Play CDN — accessed 2026-09-17
- Tailwind CSS documentation: Detecting classes in source files — accessed 2026-09-17
- GrapesJS documentation: Canvas — accessed 2026-09-17
- npm registry metadata for @tailwindcss/browser — accessed 2026-09-17
- npm registry metadata for grapesjs-tailwind (1.0.13, MIT) — accessed 2026-09-17
Written by the EditorStack Research Team. How we test and what we refuse to publish is on the methodology page.