TypeScript-first CSS builder.
- Scoped Identifiers — Class names, CSS vars and animation names are isolated
- Type-Safe Properties — All CSS properties are typed
- Minified Identifiers — Generates short, unique identifiers (class names, vars, …)
- Deterministic Builds — Minified identifiers are stored in a map file
- Multi-Language Support — Outputs modules with constant identifiers for TypeScript and Rust
- Class Maps — Efficient (interned strings) runtime state to class name mapping for conditional styles
- CSS Minification — Built-in minification via Lightning CSS
- Manifest File — Supports assetcraft manifest files (resolve urls and emit)
npm install jstyle
# or
bun add jstyleimport { ns, q, style, emit } from 'jstyle';
import * as p from 'jstyle/props';
const APP = ns('app');
const BUTTON = APP.class('button');
const rules = [
style(q.class(BUTTON), [
p.display('inline-flex'),
p.padding('8px 16px'),
p.backgroundColor('blue'),
]),
style(q.class(BUTTON).hover, [p.backgroundColor('darkblue')]),
];
await emit({
input: [{ name: 'app', build: async () => ({ css: rules }) }],
outDir: './dist',
renderURL: (name, hash) => `/assets/${name}.${hash}.css`,
});import { ns, q, style, media, $, env, important } from 'jstyle'; // Core types and factories
import * as p from 'jstyle/props'; // CSS property constructors
import { emit } from 'jstyle/emit'; // Emit orchestrator
import { JSEmitter } from 'jstyle/emit/js'; // JS emitter
import { RustEmitter } from 'jstyle/emit/rust'; // Rust emitterEach module gets its own namespace to avoid identifier collisions:
import { ns } from 'jstyle';
const SCAFFOLD = ns('app.scaffold');
const CONTAINER = SCAFFOLD.class('container');Typed constructors from jstyle/props:
import * as p from 'jstyle/props';
p.display('grid');
p.margin('0 auto');
p.color('#333');
// Predefined constants
p.FLEX; // display: flex
p.BORDER_BOX; // box-sizing: border-box// CSS variables
const SPACING = NS.dashedIdent('spacing');
style('section', [p.padding($(SPACING))]); // padding: var(--spacing);
// Environment variables
style('input', [p.padding(env('safe-area-inset-top'))]);
// !important
style('modal', [important(p.zIndex('9999'))]); // z-index: 9999 !important;Create selectors using the q factory:
const BTN = q.class('button'); // .button
const TITLE = q.tag('h1'); // h1
const LINK = q.id('nav-link'); // #nav-link
const SELF = q.self; // &
const ANY = q.universal; // *Pseudo-classes and pseudo-elements via getter properties:
BTN.hover; // .button:hover
BTN.before; // .button::before
BTN.nthChild(2, 1); // .button:nth-child(2n+1)
BTN.has(q.class('red')); // .button:has(.red)Combinators for complex selectors:
q.class('card').descendant(q.class('card_title')); // .card .card_title
q.class('card').child(q.class('card_title')); // .card > .card_titleFunctional pseudo-classes with selector arguments:
q.class('button').is([q.class('a'), q.class('b')]); // .button:is(.a, .b)
q.class('button').not(q.class('link')); // .button:not(.link)
q.class('button').where(q.class('link')); // .button:where(.link)Factory functions for all CSS at-rules:
import { media, container, keyframes, layer, supports, fontFace } from 'jstyle';
media('(min-width: 768px)', [style(container, [p.margin('0')])]);
keyframes('fade', [pct(0, [p.opacity('0')]), pct(100, [p.opacity('1')])]);
layer('utilities', [style(highlight, [p.backgroundColor('yellow')])]);
fontFace([p.fontFamily('MyFont'), p.src('url(font.woff2)')]);import { Size, rgba } from 'jstyle';
Size.px(16); // 16px
Size.rem(2); // 2rem
rgba(255, 0, 0, 1); // rgba(255, 0, 0, 1)Efficient runtime state to class name mapping:
const buttonMap = NS.classMap({
base: BUTTON,
states: {
hovered: BUTTON_HOVER,
active: BUTTON_ACTIVE,
size: [null, BUTTON_SM, BUTTON_MD, BUTTON_LG],
},
exclude: (state) => state.get('hovered') && state.get('active'),
});import { emit } from 'jstyle/emit';
import { JSEmitter } from 'jstyle/emit/js';
import { RustEmitter } from 'jstyle/emit/rust';
await emit({
input: [
{
name: 'app',
build: async (ctx) => ({
css: [
/* your rules */
],
}),
},
],
outDir: './dist',
renderURL: (name, sha) => `/assets/${name}.${sha}.css`,
emit: [
new JSEmitter({ outDir: './packages/css/src', clean: true }), // optional: emit JS modules
new RustEmitter({ outDir: './crates/css', clean: true }), // optional: emit Rust module
],
minify: true, // optional: minify CSS
map: './dist/.cssmap.json', // optional: persist ID mappings
});- CSS — Always emitted
- JS — Via
JSEmitterfromjstyle/emit/js: identifier bindings + TypeScript declarations - Rust — Via
RustEmitterfromjstyle/emit/rust: identifier bindings - Custom — Implement the
Emitterinterface fromjstyle/emit/emitterfor custom output formats
MIT OR Apache-2.0