Skip to content

localvoid/jstyle

Repository files navigation

jstyle

TypeScript-first CSS builder.

Features

  • 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)

Installation

npm install jstyle
# or
bun add jstyle

Quick Start

import { 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`,
});

Imports

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 emitter

Core Concepts

Namespaces

Each module gets its own namespace to avoid identifier collisions:

import { ns } from 'jstyle';

const SCAFFOLD = ns('app.scaffold');
const CONTAINER = SCAFFOLD.class('container');

Properties

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

Value Helpers

// 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;

Selectors

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_title

Functional 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)

At-Rules

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)')]);

Size and Color Utilities

import { Size, rgba } from 'jstyle';

Size.px(16); // 16px
Size.rem(2); // 2rem

rgba(255, 0, 0, 1); // rgba(255, 0, 0, 1)

Class Maps

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'),
});

Emitting

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
});

Output Formats

  • CSS — Always emitted
  • JS — Via JSEmitter from jstyle/emit/js: identifier bindings + TypeScript declarations
  • Rust — Via RustEmitter from jstyle/emit/rust: identifier bindings
  • Custom — Implement the Emitter interface from jstyle/emit/emitter for custom output formats

License

MIT OR Apache-2.0

About

TypeScript-first CSS builder

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors