Ramonda

Tooling, and what it does not do

Formatters and linters

No tool that parses TypeScript can read a file holding a block until it is taught, and each of them refuses rather than mangles — which is the safe half, and useless on its own:

toolwhat it says
biomeCode formatting aborted due to parsing errors
PrettierSyntaxError: ')' expected
oxlintrefuses at the parse step
esbuild, tscrefuse at the parse step

A suppression comment cannot help either: biome-ignore is read by the parser that already failed.

Prettier gets a plugin. Add it to your Prettier config and formatting works everywhere, including the format-on-save your editor does for you:

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

biome and oxlint get wrappers, because they have no plugin surface for a syntax they cannot parse:

ramonda-css format src        # your biome, your config
ramonda-css lint src          # your oxlint, your rules

Each replaces every block with something that parses, runs your own tool, and puts the block back at the indentation the tool chose. Exclude the files that hold a block from those tools' own runs, or they refuse the file before a wrapper can help.

And the editor formats the buffer, not the file. ramonda-css format --stdin-file-path <path> is what the extension runs: an editor asks a formatter about the text on screen, and a formatter pointed at a path would format what was last saved and hand back edits computed against text you have since changed.

In another JSX library

The compiled value is a value, and this framework's css prop is only one way to apply it. One exported function turns it into what any library already understands:

import { toStyleObject } from "@ramonda/css";

const panel = @@(
  display: flex;
  gap: 8px;
);

const row = <div {...toStyleObject(panel)}>a row</div>;

toStyleObject returns { className, style } — the generated class, and one entry per hole. There is no wrapper component to write and nothing to copy.

It does two things a spread cannot do for itself, and both are why it exists rather than { className: value.className, style: … } written by hand. A hole whose value is missing is left out, rather than written as the text undefined, which is a value CSS keeps. And a value holding a ; is refused, because a spread ends up in a style attribute and a server-rendered page is parsed back from HTML — measured, such a value came out of that round trip as real, applied declarations.

What you give up by spreading, and it is why this framework has a prop instead: className and style become ordinary props, so the block's class merges with whatever else writes className by whoever wrote it last, and its custom properties collide with an author's own style. The css prop is one writer of one attribute, which is a race nobody has to think about.

What it does not do

It is not CSS-in-JS. Nothing about a block is JavaScript: it is compiled away before the bundle, the class exists in a file the browser can cache, and no style is rebuilt on a render.

It does not scope your other CSS. A className is still the string you wrote, and the class in your source is the class in the served HTML — see styling.

It is not a theme system. A theme is custom properties, and switching one is an attribute on <html> — no render, no JavaScript per element, and nothing here to configure. The compiler can declare the tokens for you and check that you read them by the right name, which is a different job from owning them — see names the stylesheet sees.

It does not decide anything from the order of your classes. Nothing can — the order of names in a class attribute has no meaning in CSS, which is exactly why composition merges maps rather than concatenating class names. What decides is where you wrote something, and that is the whole point.

  • StylingclassName, style, and where stylesheets come from.
  • Performance — why a value built in the markup costs more than it looks.
  • JSX — the rest of the attribute surface.
  • Diagnostics — what the runtime says when a value reaches it that no transform produced.