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*aspfrom'jstyle/props';constAPP=ns('app');constBUTTON=APP.class('button');construles=[style(q.class(BUTTON),[p.display('inline-flex'),p.padding('8px 16px'),p.backgroundColor('blue'),]),style(q.class(BUTTON).hover,[p.backgroundColor('darkblue')]),];awaitemit({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 factoriesimport*aspfrom'jstyle/props';// CSS property constructorsimport{emit}from'jstyle/emit';// Emit orchestratorimport{JSEmitter}from'jstyle/emit/js';// JS emitterimport{RustEmitter}from'jstyle/emit/rust';// Rust emitterEach module gets its own namespace to avoid identifier collisions:
import{ns}from'jstyle';constSCAFFOLD=ns('app.scaffold');constCONTAINER=SCAFFOLD.class('container');Typed constructors from jstyle/props:
import*aspfrom'jstyle/props';p.display('grid');p.margin('0 auto');p.color('#333');// Predefined constantsp.FLEX;// display: flexp.BORDER_BOX;// box-sizing: border-box// CSS variablesconstSPACING=NS.dashedIdent('spacing');style('section',[p.padding($(SPACING))]);// padding: var(--spacing);// Environment variablesstyle('input',[p.padding(env('safe-area-inset-top'))]);// !importantstyle('modal',[important(p.zIndex('9999'))]);// z-index: 9999 !important;Create selectors using the q factory:
constBTN=q.class('button');// .buttonconstTITLE=q.tag('h1');// h1constLINK=q.id('nav-link');// #nav-linkconstSELF=q.self;// &constANY=q.universal;// *Pseudo-classes and pseudo-elements via getter properties:
BTN.hover;// .button:hoverBTN.before;// .button::beforeBTN.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_titleq.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);// 16pxSize.rem(2);// 2remrgba(255,0,0,1);// rgba(255, 0, 0, 1)Efficient runtime state to class name mapping:
constbuttonMap=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';awaitemit({input: [{name: 'app',build: async(ctx)=>({css: [/* your rules */],}),},],outDir: './dist',renderURL: (name,sha)=>`/assets/${name}.${sha}.css`,emit: [newJSEmitter({outDir: './packages/css/src',clean: true}),// optional: emit JS modulesnewRustEmitter({outDir: './crates/css',clean: true}),// optional: emit Rust module],minify: true,// optional: minify CSSmap: './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