Back to all work
CSS · Tools · Frontend

SafiCSS

A tiny runtime CSS-in-JS library for plain HTML. Write your styles as a JavaScript object, call css(), and it injects a class and hands back its name. It reaches all of CSS: hover, media queries, and keyframes, not only inline styles. No build step, no framework. Works from npm or a CDN, about 1.3 KB gzipped, zero runtime dependencies.

Javascript Typescript
Role
Solo developer
Timeline
1 day
Year
2026
Status
Completed
SafiCSS
01 / The overview

SafiCSS styles plain HTML by letting you write a style object in JavaScript and call one function. css(object) turns the object into real CSS, generates a class name, injects the rules into a single style tag, and returns the class. The same object always gets the same class, so nothing is duplicated. Because it injects a class instead of an inline style string, pseudo-classes like hover, media queries, and keyframe animations all work. It ships on npm and over a CDN, as an ES module or a plain script global, is about 1.3 KB gzipped, and has zero runtime dependencies. It is written in TypeScript and ships types, so your editor autocompletes property names and flags a bad value or a typo.

02 / The challenge

The idea started from wanting React's style object syntax in a plain HTML file with no build step. That exact syntax is JSX, and a browser reading raw HTML treats it as a string, not an object, so it needs a compiler. Inline styles, the one thing you can set from JavaScript with no build, cannot express hover, media queries, or animations. So writing CSS as a JS object in plain HTML and reaching all of CSS pull against each other, unless the tool does more than set an inline style.

03 / The solution

SafiCSS is a runtime function, not a compiler, so nothing has to build on the user's side. css(object) serializes the object into a stable string, hashes it into a class name, and injects the rules once into a single style element, keeping a record of what it already wrote so repeat calls cost nothing. camelCase keys become kebab-case, numbers get px except for the properties that are unitless, and custom properties are left untouched. Object values become nested blocks: a key with & is the generated class (like "&:hover" or "& span"), and a key with @ is an at-rule (like "@media"), which is how it reaches all of CSS. keyframes() injects an @keyframes block and returns the name, injectGlobal() writes resets and root variables, and setVars() sets custom properties at runtime so you can change a value live with no re-injection. The types are built on csstype, the same base React uses, so a typo in a property name is a type error instead of dead CSS. It builds to ESM, CommonJS, and an IIFE global with tsup, and serves from npm and from jsDelivr straight off the GitHub repo.

FAQ

About this project

A tiny runtime CSS-in-JS library for plain HTML. You write a style object, call css(), and it returns a class name after injecting the rules once. No build step, and it works from npm or a CDN.

Inline styles can't do hover, media queries, or animations. SafiCSS injects a real class, so pseudo-classes, media queries, and keyframes all work, while you still write a plain JS object.

No. It runs in the browser at runtime. Drop in a script tag, import it from a CDN, or install from npm for a bundler. It works with plain HTML and with any framework.

About 1.3 KB gzipped, with zero runtime dependencies.

Yes. The style object is normal JavaScript, so any variable works inside it. For values you want to change live, put them in a CSS custom property and call setVars to update it with no re-injection.

Yes. It ships types built on csstype, so property names autocomplete and a typo or bad value is a type error. That is the linting for the style object, no stylelint needed.

Next project S-Calc

Want something like this?

Start a conversation →