A Notion-style editor that does not bring a CSS framework
Installing a rich text editor should not decide how your application is styled. How Blockleaf themes with fifty-seven custom properties, and why that still works inside Tailwind and shadcn/ui.
There is a line in a lot of editor documentation that reads roughly: add
@tailwindcss/typography and wrap the content in prose.
It is a sensible instruction and it is also a dependency, a plugin, a build step and a set of opinions about type scale, arriving because you wanted a text box.
Blockleaf ships one stylesheet of plain CSS.
import '@blockleaf/editor/styles.css';No Tailwind, no prose, no PostCSS plugin, no content globs to register,
nothing to configure. The package contains no !important, and the build
fails if anyone adds one.
Why this is a product decision
An editor is unusual among dependencies: it renders a large, visible surface in the middle of your application. A date picker can look a bit foreign in the corner. A page of body copy cannot.
So an editor has three options. Ship no styles, and everyone writes a stylesheet from scratch. Ship styles coupled to a framework, and everyone adopts the framework. Or ship styles designed to be overridden, and make the overriding cheap.
The third one is the only one that does not impose anything, and "cheap" is doing the work in that sentence. If re-theming means forking a stylesheet, you have shipped option one with extra steps.
Custom properties, not class names
Every colour, size and radius in Blockleaf resolves through a --bl-*
property. A theme is a declaration block:
.blockleaf-editor {
--bl-color-accent: #3b82f6;
--bl-color-bg: #fbfbfd;
--bl-font-body: 'Inter', system-ui, sans-serif;
--bl-radius: 10px;
}Fifty-seven properties cover the content surface, the slash menu, the bubble menu, the block menu, the drag handle and the table controls. The menu groups fall back to the content colours, so changing four variables changes everything that should follow.
Below that there are stable, BEM-flavoured class names —
.blockleaf-editor__content, .blockleaf-slash-menu__item,
.blockleaf-bubble-menu__button — at one class of specificity, so your own
sheet wins on order. And below that, every piece of chrome is a React
component you can replace outright.
Three levels, and most people never leave the first.
This is not an anti-Tailwind position
Half the people reading this use Tailwind, and the two things compose fine — this site is Fumadocs on Tailwind 4 with the editor's stylesheet imported next to it.
The trick is that Tailwind 4 exposes its theme as CSS variables, so wiring the design system into the editor is one block:
.blockleaf-editor,
.blockleaf-slash-menu {
--bl-color-accent: var(--color-brand);
--bl-color-fg: var(--color-gray-900);
--bl-color-border: var(--color-gray-200);
--bl-font-body: var(--font-sans);
--bl-radius: var(--radius-lg);
}shadcn/ui is the same shape, pointing at --primary, --foreground,
--border and --radius. Change the design system, and the editor changes
with it, without anyone remembering that the editor exists.
Full recipes: with Tailwind, with shadcn/ui.
Two details that cost people an afternoon
Do not also apply prose. Tailwind Typography styles the same elements
at the same specificity, so the winner is whichever stylesheet loaded last,
which is not a stable way to run a design.
Include the slash menu in your selector. It is mounted on
document.body rather than inside the editor wrapper, so variables set on
.blockleaf-editor do not reach it. Set them on :root, or list
.blockleaf-slash-menu alongside. Everything else — the bubble menu, the
drag handle, the table controls — lives inside the wrapper and inherits.
Preflight does not break it
The other thing people expect to break is a CSS reset eating the editor's
list markers and heading sizes. It does not, for a boring reason: the
package's rules are scoped to a class, so
.blockleaf-editor__content h1 at specificity 0,1,1 beats Preflight's
h1 at 0,0,1 regardless of order.
That one is not theory. An early version of this stylesheet inherited
list-style from the user agent, which meant bullets vanished in any
application with a reset — and "works without a CSS framework" should not
mean "breaks with one". It is stated explicitly now.
Dark mode
Out of the box the stylesheet follows prefers-color-scheme, and
data-theme="light" or data-theme="dark" on the wrapper overrides it.
One honest limitation while this is still 0.0.x: the editor component
writes its own wrapper and does not forward data-theme yet, so an
application with a manual light/dark toggle has to re-declare the palette
under its own selector. Theming has
the four lines, and it is a gap rather than a design.
Further reading
- The style guide — the three levels, with examples.
- Theming — all fifty-seven properties.
- Why Blockleaf exists.