- Import.
with { type: "cssints" } marks code that runs at build time: cssints itself, plugins such as icons, and your own helper modules. The plugin finds call sites by this attribute. The one runtime helper, cx, is imported without it. Using a build-time member without the attribute is a build error.
- Names. Every mdn property as camelCase (
paddingLeft, alignItems, justifyContent), plus a short set: p pt pr pb pl, m mt mr mb ml, bg. Everything is available both as named exports and through the namespace (css.p). There is no items or justify: they were ambiguous (align-items or align-content, justify-content or justify-items).
- Numbers. A bare number is a spacing step of 0.25rem in
padding*, margin*, gap* and inset* (p(4) is 1rem). In any other property it is a CSS <number>: opacity(0.5), zIndex(3).
- Values. A property takes one or more parts joined by a space. Each part is a string or a Typed OM value:
border("1px solid", accent), padding(CSS.rem(1), CSS.rem(2)), padding("1rem 2rem"). Parts are checked against the mdn grammar in types and again at build time. CSS.px, rem, em, percent, number, CSSUnitValue and CSSColorValue come from cssints, imported with the attribute.
- Editor. A bad part is marked on that part:
padding("2px", "red") puts the error on "red", with the message of the whole value. While a string is not a value yet, the editor offers the keywords of the property that the word under the cursor begins (display("fl…") gives flex, flow, flow-root; border("1px", "so…") gives solid), and all of them in an empty part (border("1px", ""); an empty part is an error). The offer is the keywords of the whole grammar, not only those that fit at that point, it stops once the string is valid, and functions such as rgb( are not offered.
- Math.
calc(), min(), max(), clamp() and the other CSS Values 4 math functions are accepted wherever their type is, as strings (width("calc(100% - 2rem)"), checked by the build: calc(1px + 2s) is an error) and as calls that take tokens (calc(t.space.md, "*", 2), see "Functions").
- Layout contexts.
flex() and grid() without arguments set display and chain only their container properties. Names drop only the context prefix: flex().direction("column").wrap("wrap").alignItems("center").justifyContent("space-between"), grid().templateColumns("1fr 2fr").gap(4). Plain styles do not chain; merge them with cn(...) or cx(...). Item properties (flexGrow, alignSelf, gridColumn, order) are plain functions.
flex(...parts) and grid(...parts) are the shorthand properties. With at least one argument the call is the flex (or grid) property, checked by its grammar like any member: flex("1"), flex(1) and flex("1 1 0") are flex: 1, flex: 1 and flex: 1 1 0, grid("auto / 1fr 1fr") is grid: auto / 1fr 1fr. A bare number is a <number> here, not a spacing step. The result is a plain style: it sets no display and does not chain. The types tell the two forms apart by overload.
container is the condition. container("sidebar (width < 30rem)") is a query; the container shorthand property is written with its longhands containerName and containerType.
- Arguments are build-time values. The plugin runs your module at build time and replaces each call with a class string. A literal, a
const, a token or a value from another attributed module works. A value that exists only at runtime (a prop, state) does not: p(n) with n a parameter is a build error (css arguments must be known at build time: n is not defined where the call is evaluated) that points at the call, also when the local is named like a global (open, name) or like a module binding. The types do not catch it: a helper module may take parameters (below), so a string or number is a fine argument there. Choose between styles with cx(on && p(4)), a cv variant, or a token set in a style prop. A helper module imported with the attribute may take parameters, because each call of it is a site with its own arguments: stack(4) below is a build-time call in the file that makes it.
- Top-level code runs at build time. A call the run does not reach (
false && p(4), a branch not taken, a ternary arm) still gets its class string: it is evaluated after the module has loaded, like a call inside a function, so its arguments may use the bindings of the module but not the locals of the branch. A cv(...) at the top of a module works too: during the build it is the same function the browser gets, so button({ size: "lg" }) at the top of a module gives its classes.
// stack.ts
import * as css from "cssints" with { type: "cssints" };
export const stack = (gap: number) => css.flex().direction("column").gap(gap);
// elsewhere: import { stack } from "./stack.ts" with { type: "cssints" }; ... className={stack(4)}