Ramonda

Values that come from data

A block is compiled before your app runs. Everything in it becomes a class that already exists in the stylesheet, which is what makes two elements with the same styles share one rule.

A value your app computes at run time cannot be in a class, because the class was written before the value existed. A progress bar's width, a colour chosen by somebody, the offset of a row in a virtualised list — each of those is one element's own value, and CSS has one place for that: a custom property, set on the element.

Declare it, read it in the block, and set it where the element is written.

Declaring one

const width = @@property(
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
);

That is a real @property rule in the stylesheet, and the binding is its name — the compiler generates one, so it cannot collide with anything and cannot be mistyped.

Reading it

const width = @@property( syntax: "<percentage>"; initial-value: 0%; inherits: false; );

const bar = @@( height: 8px; background: $.color.accent; width: var({width}); );

var({width}) is resolved when the block compiles, so width is written into the rule as text. Nothing about this declaration is per-element: bar is one class, shared by every bar on the page.

Setting it

<div className={bar} style={{ [width]: `${this.done}%` }} />

The binding is the property's name, so it goes straight in as a key.

toStyle checks the value against the syntax you declared

A plain object key takes any string, so the line above is as checked as any other object. toStyle is the same set with the type kept:

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

const done = @@property( syntax: "<percentage>"; initial-value: 0%; inherits: false; );
const meter = @@( height: 8px; width: var({done}); );

const Bar = (props: { at: number }) => (
  <div className={meter} style={toStyle([[done, `${props.at}%`]])} />
);
toStyle([[angle, "45deg"]]);     ✓
toStyle([[angle, "45px"]]);      ✗  a length where `<angle>` was declared

It takes several at once, and it takes declared variables — $.color.accent — in the same list, so one call sets everything an element carries.

The binding's type is CssVar<"angle">, read from the syntax you wrote. That is also what a function annotates when it takes any angle property this app registered:

import type { CssVar } from "@ramonda/css";
import { toStyle } from "@ramonda/css";

const spin = (name: CssVar<"angle">, deg: number) => toStyle([[name, `${deg}deg`]]);

A syntax the type system has no kind for — "*", or a compound grammar like "<length> | auto" — binds CssVar<"any">, which takes what CSS itself would.

A property nothing sets is reported

initial-value is required, so a property nothing ever sets still renders — every element gets the initial, the page looks fine, and nothing says the value you meant to vary never arrives. So it is a finding:

src/Dial.tsx:1:15  registered-never-set
  `angle` is read by a block and set by nothing, so every element gets its `initial-value`.

Anything that mentions the binding in TypeScript counts as setting it — style, toStyle, a helper you pass it to. The rule fires only when the name is written in exactly one place, its own declaration, and read from blocks.

One shape it is wrong about: a library that exports a registered property for its consumers to set. Those consumers are not in the program, so there is nothing for it to see. Put a // ramonda-css-ignore <reason> on the line above the declaration, or turn the rule off in ramonda.css.ts.

Declared once, read anywhere

A declared property is one variable however many declarations read it. A block reading one property twice compiles to:

@property --r-pHqJVsKzI { syntax:"<length>";initial-value:0px;inherits:false; }
.r-pl-var\(--r-pHqJVsKzI\) { padding-left:var(--r-pHqJVsKzI); }
.r-pr-var\(--r-pHqJVsKzI\) { padding-right:var(--r-pHqJVsKzI); }

One name, two classes reading it. So you set it once and every declaration that reads it moves together.

What syntax buys, beyond tidiness

The browser checks the value. A property declared <percentage> and handed 12px is not applied at all, and the initial-value stands instead — so a wrong value degrades to a known one rather than to nothing.

It can be animated. A custom property the browser knows nothing about is a string, and a string cannot be interpolated. A registered one has a type, so transition: width 200ms on a property the browser understands actually moves:

const angle = @@property( syntax: "<angle>"; initial-value: 0deg; inherits: false; );

const dial = @@(
  transform: rotate(var({angle}));
  transition: transform 200ms ease-out;
);

initial-value is a real fallback. An element that never sets the property still renders, with the value you declared — not with the declaration missing.

A name nobody else can take

A custom property inherits. A card setting --accent changes every descendant whose own block reads that name, which is a feature when you mean it and a collision when you do not.

The compiler's names cannot collide by accident: each is derived from the declaration itself, so two different properties never agree on one. Declaring inherits: false closes the other half — the value then reaches the element it was set on and no further.

Across a component boundary, send the VALUE

A component that takes a style from its parent takes a block — see styles a caller may send. A component that varies with data takes the data:

const width = @@property( syntax: "<percentage>"; initial-value: 0%; inherits: false; );
const bar = @@( height: 8px; width: var({width}); );

class Bar extends Component<{ done: number }> {
  render() {
    return <div className={bar} style={{ [width]: `${this.props.done}%` }} />;
  }
}

done is a number. A number is the same value on every render that computes it the same way, so nothing downstream has to decide whether it changed — where an object built in the markup is a new one each time, and everything holding it has to look inside to find out that nothing moved.

What this costs, honestly

Two places instead of one: the block says what reads the value, and the element says what it is. Nothing in the language ties them together, so a block that reads a property whose element never sets it renders with the initial-value and says nothing about it.

That is the trade for a stylesheet that is written once and an element that carries only what is truly its own.

Next