Not on npm yet. In this repo cssints is a workspace package (apps/demo depends on "cssints": "workspace:*"). It needs Vite 8 (Vite+ works too) and, for the plugin, Node with type stripping (22.18 or later).
// vite.config.ts
import function cssints({ inner, exclude, devtools, opencode, }?: {
inner?: boolean;
exclude?: string | RegExp | ReadonlyArray<string | RegExp>;
devtools?: boolean | "build";
opencode?: boolean | OpenCodeOptions;
}): Plugin[]
cssints, { const cssTarget: string[]`build: { cssTarget }`, the browserslist query of `targets`, so a minified build keeps `firstThatWorks` fallbacks.cssTarget, const targets: TargetsLightningCSS targets (css.lightningcss.targets) from the browserslist config of process.cwd(), read when vite.config.ts
imports this module: the support warnings read the one of the Vite root, the same unless the root is elsewhere.targets } from "cssints/vite";
export default {
plugins: Plugin<any>[][]plugins: [function cssints({ inner, exclude, devtools, opencode, }?: {
inner?: boolean;
exclude?: string | RegExp | ReadonlyArray<string | RegExp>;
devtools?: boolean | "build";
opencode?: boolean | OpenCodeOptions;
}): Plugin[]
cssints()],
css: {
transformer: string;
lightningcss: {
targets: Targets;
};
}
css: { transformer: stringtransformer: "lightningcss", lightningcss: {
targets: Targets;
}
lightningcss: { targets: Targetstargets } },
build: {
cssMinify: string;
cssTarget: string[];
}
build: { cssMinify: stringcssMinify: "lightningcss", cssTarget: string[]cssTarget },
};// the entry, once
import "virtual:cssints.css";targets and cssTarget come from the browserslist query of the support warnings: the BROWSERSLIST variable, else the standard config ("browserslist" in package.json or .browserslistrc, with the BROWSERSLIST_ENV, NODE_ENV or production section), else defaults. The exports are computed when vite.config.ts is imported, before Vite's root is known, so they look the config up from process.cwd() and upward; the support warnings look it up from Vite's root (2026-10-09, cssints-p3d2). They are the same when Vite runs from the app's directory; with vite <dir> from elsewhere, give targets and cssTarget the app's config yourself. Vite's CSS minify reads cssTarget, not targets: without it the minifier merges the declarations of a firstThatWorks into the last one. Add "types": ["vite/client"] to tsconfig.json for the virtual: import.
Any framework that builds JSX through a Vite plugin works; the engine evaluates your modules itself, so it does not care what the plugin makes of JSX, nor in which order the plugins are listed. apps/sky is the Solid 2 app (solid-js and @solidjs/web 2.0.0-rc.13, @solidjs/vite-plugin 3.0.0-next.47):
// vite.config.ts
import solid from "@solidjs/vite-plugin";
import cssints, { cssTarget, targets } from "cssints/vite";
export default {
plugins: [cssints(), solid()],
css: { transformer: "lightningcss", lightningcss: { targets } },
build: { cssMinify: "lightningcss", cssTarget },
};tsconfig.json is Solid's: "jsx": "preserve", "jsxImportSource": "@solidjs/web", "types": ["vite/client"]. Solid's class takes a string, an object or an array, so cx is not needed: class={[card, on() && p(4)]} and class={badge({ tone: tone() })} work as in React. A site's arguments are still build-time values, so a signal goes in the condition or in a cv variant, not in p(n()). Under vite dev an edit of a component replaces it and resets its state below it (Solid's refresh); an edit of a style changes the class strings and the sheet with no reload.
import { const cx: (...a: ClassValue[]) => stringcx } from "cssints";
import { const bg: Prop<"background-color">`<color>`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/background-color)bg, const color: Prop<"color">`<color>`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/color)color, const cn: (...styles: Style[]) => Stylecn, const cv: <V extends Record<string, Record<string, Style>> = Record<never, never>>(config: CvConfig<V>) => (props?: CvProps<V>) => stringThe plugin replaces the call by a runtime function of class strings: `button({ size: "lg" })` gives the class names.cv, const flex: Layout<"flex", FlexMethods>`none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]`. Chrome 29, Edge 12, Firefox 22, Safari 9, iOS 9, Android 29. [MDN](https://developer.mozilla.org/docs/Web/CSS/flex)flex, const hover: StateWrapChrome 1, Edge 12, Firefox 1, Safari 2, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/:hover)hover, const media: Condition<"media", QueryWrap>Query strings are checked against the mdn-data grammar: `media("(width >= 40rem)")`. Several give a tuple, an object an object.media, const p: Prop<"padding">`<'padding-top'>{1,4}`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/padding)p, const style: (...styles: (Style | StyleObject)[]) => StyleObjectcss values (declarations, no conditions) and `token.set()` results, as an inline `style` object.style, const token: {
readonly number: TokenFn<"number">;
readonly color: TokenFn<"color">;
readonly length: TokenFn<"length">;
readonly percentage: TokenFn<"percentage">;
readonly integer: TokenFn<"integer">;
readonly angle: TokenFn<"angle">;
readonly time: TokenFn<"time">;
readonly resolution: TokenFn<"resolution">;
readonly url: TokenFn<"url">;
readonly image: TokenFn<"image">;
readonly shadow: TokenFn<"shadow">;
readonly lengthPercentage: TokenFn<"lengthPercentage">;
readonly customIdent: TokenFn<...>;
readonly transformList: TokenFn<...>;
readonly easingFunction: TokenFn<...>;
} & {
...;
}
`token.<type>(initial, name?)`: a Typed OM value of the type, `var(--name)`. With a name it can be set.token } from "cssints" with { type: "cssints" };
const const md: QueryWrapmd = media<"(width >= 40rem)">(query: "(width >= 40rem)"): QueryWrap (+2 overloads)Query strings are checked against the mdn-data grammar: `media("(width >= 40rem)")`. Several give a tuple, an object an object.media("(width >= 40rem)");
const const accent: Token<"color", true, string>accent = const token: {
readonly number: TokenFn<"number">;
readonly color: TokenFn<"color">;
readonly length: TokenFn<"length">;
readonly percentage: TokenFn<"percentage">;
readonly integer: TokenFn<"integer">;
readonly angle: TokenFn<"angle">;
readonly time: TokenFn<"time">;
readonly resolution: TokenFn<"resolution">;
readonly url: TokenFn<"url">;
readonly image: TokenFn<"image">;
readonly shadow: TokenFn<"shadow">;
readonly lengthPercentage: TokenFn<"lengthPercentage">;
readonly customIdent: TokenFn<...>;
readonly transformList: TokenFn<...>;
readonly easingFunction: TokenFn<...>;
} & {
...;
}
`token.<type>(initial, name?)`: a Typed OM value of the type, `var(--name)`. With a name it can be set.token.color: <"#2563eb", "accent">(initial: "#2563eb", name?: "accent" | undefined) => Token<"color", true, string>color("#2563eb", "accent");
const const button: (props?: CvProps<{
wide: {
true: Style;
};
}> | undefined) => string
button = cv<{
wide: {
true: Style;
};
}>(config: CvConfig<{
wide: {
true: Style;
};
}>): (props?: CvProps<{
wide: {
true: Style;
};
}> | undefined) => string
The plugin replaces the call by a runtime function of class strings: `button({ size: "lg" })` gives the class names.cv({
CvConfig<V extends Record<string, Record<string, Style>>>.base?: Style | undefinedbase: function cn(...styles: Style[]): Stylecn(p<readonly [2]>(parts_0: 2): Style`<'padding-top'>{1,4}`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/padding)p(2), color<readonly ["white"]>(parts_0: "white"): Style`<color>`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/color)color("white"), bg<readonly [Token<"color", true, string>]>(parts_0: Token<"color", true, string>): Style`<color>`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/background-color)bg(const accent: Token<"color", true, string>accent), function hover(...styles: Style[]): StyleChrome 1, Edge 12, Firefox 1, Safari 2, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/:hover)hover(bg<readonly ["#1d4ed8"]>(parts_0: "#1d4ed8"): Style`<color>`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/background-color)bg("#1d4ed8"))),
CvConfig<{ wide: { true: Style; }; }>.variants?: {
wide: {
true: Style;
};
} | undefined
variants: { wide: {
true: Style;
}
wide: { true: Styletrue: const md: (...styles: Style[]) => Stylemd(p<readonly [4]>(parts_0: 4): Style`<'padding-top'>{1,4}`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/padding)p(4)) } },
});
export const const Row: ({ wide, on }: {
wide?: boolean;
on?: boolean;
}) => any
Row = ({ wide: boolean | undefinedwide, on: boolean | undefinedon }: { wide?: boolean | undefinedwide?: boolean; on?: boolean | undefinedon?: boolean }) => (
<IntrinsicElements[string]: anydiv className?: string | undefinedclassName={function cx(...a: ClassValue[]): stringcx(function flex(): Chain<FlexMethods> (+1 overload)The layout context: sets `display` and chains its container properties.flex().alignItems: <readonly ["center"]>(parts_0: "center") => Chain<FlexMethods>`normal | stretch | <baseline-position> | [ <overflow-position>? <self-position> ] | anchor-center`. Chrome 29, Edge 12, Firefox 20, Safari 9, iOS 9, Android 29. [MDN](https://developer.mozilla.org/docs/Web/CSS/align-items)alignItems("center").gap: <readonly [2]>(parts_0: 2) => Chain<FlexMethods>`<'row-gap'> <'column-gap'>?`. Chrome 57, Edge 16, Firefox 52, Safari 10.1, iOS 10.3, Android 57. [MDN](https://developer.mozilla.org/docs/Web/CSS/gap)gap(2), on: boolean | undefinedon && p<readonly [1]>(parts_0: 1): Style`<'padding-top'>{1,4}`. Chrome 1, Edge 12, Firefox 1, Safari 1, iOS 1, Android 18. [MDN](https://developer.mozilla.org/docs/Web/CSS/padding)p(1))} style?: Record<string, string> | undefinedstyle={function style(...styles: (Style | StyleObject)[]): StyleObjectcss values (declarations, no conditions) and `token.set()` results, as an inline `style` object.style(const token: {
readonly number: TokenFn<"number">;
readonly color: TokenFn<"color">;
readonly length: TokenFn<"length">;
readonly percentage: TokenFn<"percentage">;
readonly integer: TokenFn<"integer">;
readonly angle: TokenFn<"angle">;
readonly time: TokenFn<"time">;
readonly resolution: TokenFn<"resolution">;
readonly url: TokenFn<"url">;
readonly image: TokenFn<"image">;
readonly shadow: TokenFn<"shadow">;
readonly lengthPercentage: TokenFn<"lengthPercentage">;
readonly customIdent: TokenFn<...>;
readonly transformList: TokenFn<...>;
readonly easingFunction: TokenFn<...>;
} & {
...;
}
`token.<type>(initial, name?)`: a Typed OM value of the type, `var(--name)`. With a name it can be set.token.set: <"color", "#dc2626">(token: Token<"color", true, string>, value: "#dc2626") => StyleObjectset(const accent: Token<"color", true, string>accent, "#dc2626"))}>
<IntrinsicElements[string]: anybutton className?: string | undefinedclassName={const button: (props?: CvProps<{
wide: {
true: Style;
};
}> | undefined) => string
button({ wide?: boolean | undefinedwide })}>Save</IntrinsicElements[string]: anybutton>
</IntrinsicElements[string]: anydiv>
);with { type: "cssints" } marks what runs at build time. The plugin replaces each call with a class string, so the browser gets cx("_1s47490 _1ob5z5m _g2iyum", on && "_lqk467") and button is a call of the runtime with { base: "_1t8gqy5 ...", ... }, so it never sees flex(). virtual:cssints.css is one sheet with every rule once, sorted by layer, then condition, then shorthand before longhand, then class name:
@layer _.k, _.a, _.b, _.c, _.d, _.e;
@property --accent{syntax:"<color>";inherits:true;initial-value:#2563eb}
@layer _.a {
._1t8gqy5{padding:0.5rem}
._lqk467{padding:0.25rem}
._g2iyum{gap:0.5rem}
._17xzvrh{background-color:var(--accent)}
._1binn6n{color:white}
._1ob5z5m{align-items:center}
._1s47490{display:flex}
}
@layer _.b {
._1tsjws2:hover{background-color:#1d4ed8}
@media (width >= 40rem) {
._tdcza3{padding:1rem}
}
}import * as css from "cssints/tui" with { type: "cssints" } gives the members a terminal has (cells, colours, borders, flex, states, cursor), and the plugin compiles them to prop objects for OpenTUI renderables instead of classes; @cssints/tui applies their states and themes: packages/tui, docs/API.md "TUI".
More in docs/API.md: property names and short names, flex() and grid() chains, conditions and layers, tokens and themes, cx, cn and cv, and the errors you can get.