Ramonda

Style blocks

Write CSS where the element is, in CSS:

<div css={@@(
  display: flex;
  gap: 8px;
  border-left: 4px solid #10b981;
  &:hover { border-left-color: #00b37e; }
)}>
  Online
</div>

Nothing about that is a string, an object, or a template literal. It is CSS, and it is checked as CSS — a misspelled property, a value the property does not take, and a unit your project does not use are all reported where you wrote them, before the build runs.

It compiles away. Each declaration becomes one class in a stylesheet, so the element above ships as class="r-disp-flex r-gap-8px …" and the CSS is a file the browser caches. There is no runtime, and nothing is computed while your page renders.

@ramonda/css is a separate package. The framework's own position on styling — a className, a style, stylesheets the way they have always arrived — is on Styling, and it does not change if you never install this.

Install

npm install @ramonda/css

The checking is TypeScript's own, so the package needs yours — it writes a virtual file your compiler reads, and a fault in a block arrives as an ordinary tsc diagnostic rather than as a report from a tool you have to run separately. Any TypeScript 5 will do.

One plugin in the build

The CSS a block compiles to is a module the bundler already knows about, and it follows the JavaScript chunk it belongs to — so there is nothing to import for it. (A project that declares variables imports one stylesheet, once, for those.)

import { ramondaCss } from "@ramonda/css/vite";

export const plugins = [ramondaCss()];

esbuild builds the same thing:

import { ramondaCss } from "@ramonda/css/esbuild";

export const plugins = [ramondaCss({ filter: /src\/.*\.tsx$/ })];

Set filter on esbuild. esbuild hands a plugin a path rather than the code, so a file has to be read before it can be asked whether it holds a block — measured at 17 µs a file. Pointing the plugin at the tree that holds them means nothing else is opened at all.

If you declare variables

Nothing above changes, and one thing is added: the plugin writes a css-system/ folder beside ramonda.css.ts holding $ and the values, and your app imports the stylesheet once.

import "./css-system/variables.css";

Commit that folder. It is generated, and it is also what your editor reads, so a fresh clone that has not built anything yet is still checked. npx ramonda-css codegen writes it without a build, and --check fails in CI when what is committed no longer matches the config beside it.

A route that is already code-split gets its own stylesheet, without being asked. A block belongs to the module it was written in and each module imports its own CSS, so splitting is a decision the bundler was making anyway: a lazily-loaded module gets its own .css asset, carrying that module's rules and not the entry's.

Two things in your editor

They are separate on purpose, and they answer different halves: the plugin decides what is an error, the extension decides what you see while typing.

The compiler, for completion, hover and the red squiggles

A TypeScript language-service plugin, turned on in your own tsconfig.json:

{ "compilerOptions": { "plugins": [{ "name": "@ramonda/css/plugin" }] } }

Your editor has to be running the workspace's TypeScript for any plugin to load. In VS Code: TypeScript: Select TypeScript Version → Use Workspace Version.

The extension, for colours, formatting, and the second TypeScript server

The colours are a TextMate grammar, which is why they are an extension and not part of the plugin: a grammar needs no program and costs nothing. The extension does two more things — it runs your own formatter over a block, and it carries the plugin to the editor's second TypeScript server.

code --install-extension ramonda.css

Or search Ramonda CSS in the Extensions panel.

Formatting then depends on what your project already uses, and the two answers are opposite.

Using Prettier? Keep Prettier, and do not touch editor.defaultFormatter. @ramonda/css ships a Prettier plugin, so your usual formatter handles these files with nothing else to configure:

{ "plugins": ["@ramonda/css/prettier"] }

Using biome? biome has no plugin surface for a syntax it cannot parse, so the formatting goes through this extension, which runs your biome with your config:

{
  "[typescriptreact]": { "editor.defaultFormatter": "ramonda.css" },
  "editor.formatOnSave": true
}

What that setting does, so you can decide where to put it. editor.defaultFormatter names the one extension VS Code asks for that language, and it does not fall through to another — so it reaches every project it is in scope for:

where the setting isin a biome projectin a Prettier projectin a project without @ramonda/css
the project's .vscode/settings.jsonwhat you want
your user settingswhat you wantruns biome, not Prettierformat-on-save does nothing: this extension has no command to run and returns no edits, and Prettier and biome are never asked

In a workspace it is also what everyone else on the project gets.

TypeScript's own formatter steps aside for a file that holds a block, which is why one of the two above is not optional. The syntax is not TypeScript, and an edit the language service computed for it would land on the wrong characters and corrupt the file rather than merely look wrong — so it is refused for the whole file. In a file with a block, Format Document and Format Selection do nothing by themselves, even on lines nowhere near the block.

Why the extension is not only colours

An editor runs two TypeScript servers — a syntax one for what needs no types, and a semantic one for everything else. The syntax one owns formatting, code folding, the outline and expand-selection for as long as the editor is open, and it never opens your tsconfig.json. So a plugin named there never reaches it, and it reads your file as plain TypeScript. Your file is not plain TypeScript, and it walks into an assertion of its own. From a real editor's log:

[error] [vscode.typescript-language-features] provider FAILED
[error] Error: <syntax> TypeScript Server Error (5.9.3)
Debug Failure. False expression: Token end is child end

The extension is what reaches it. It carries the same plugin a second way, which VS Code hands to both servers — so formatting a block leaves it alone, and the outline matches. With the extension installed there is nothing to configure.

Without it, one setting is needed, and it turns the syntax server off:

{ "js/ts.tsserver.useSyntaxServer": "never" }

Nothing is lost by that: one server answers everything the two did. What the syntax one was buying is speed on a cold project — folding, the outline, Format Document and expand-selection answer before the program has loaded rather than after it. That is one wait, once per project. Older VS Code spells the setting typescript.tsserver.useSyntaxServer, and either is read.

Check that it worked

Write a block with a property that does not exist:

const wrong = <div css={@@( dsiplay: flex; )}>x</div>;

Your editor should underline dsiplay and offer display. If it does, the plugin is loaded and the workspace TypeScript is the one running. If the whole line is red instead, the syntax server is still being asked — that is the setting above.

Where to go next

  • Writing a block — where a block goes, holes for values that change, nesting and conditions.
  • What is checked — every rule, what it catches, and how to silence one that is wrong.
  • Names the stylesheet sees — declaring variables and reading them with $, keyframes and font faces, and what a theme is.
  • Which declaration wins — two declarations of one property, and the rule that decides between them.
  • Composing — reusing a block, conditions, and your own stylesheet.
  • The config fileramonda.css.ts end to end, and what a project can decide not to allow.
  • Project settings — the names a block emits, and who reads them.
  • Tooling — formatters, linters, other JSX libraries, and what this does not do.