Tiny runtime CSS-in-JS for plain HTML. You write your styles as a JavaScript object, call one function, and it styles the element. No build step, no framework. Drop it in any HTML file over a CDN, or install it from npm. It reaches all of CSS: hover, media queries, and keyframes, not only inline styles.
css(object) turns the object into real CSS, generates a class name, injects the rules into one <style> tag, and hands back 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, media queries, and animations all work.
- Zero runtime dependencies
- Ships TypeScript types, so your editor autocompletes property names and flags bad values
- About 1.3 KB gzipped
- Works from npm and from a CDN, as an ES module or a plain
<script>global
npm i saficssimport { css } from "saficss";
document.getElementById("box").className = css({
background: "#444",
color: "#fff",
padding: 16,
});<div id="box"></div>
<script type="module">
import { css } from "https://esm.sh/saficss";
const accent = "#22d3ee"; // a normal JS variable
document.getElementById("box").className = css({
background: "#444",
color: accent,
padding: 16,
borderRadius: 8,
"&:hover": { background: "#666" },
"@media (max-width: 600px)": { padding: 8 },
});
</script>Load the IIFE build and use window.SafiCSS:
<div id="box"></div>
<script src="https://cdn.jsdelivr.net/npm/saficss/dist/saficss.global.js"></script>
<script>
document.getElementById("box").className = SafiCSS.css({
background: "#444",
color: "#fff",
padding: 16,
});
</script>jsDelivr serves any file in the repo without an npm publish. Point at a tag or commit for a stable URL:
<!-- ES module -->
<script type="module">
import { css } from "https://cdn.jsdelivr.net/gh/Abdulkader-Safi/SafiCSS@v0.1.0/dist/saficss.mjs";
</script>
<!-- Global script -->
<script src="https://cdn.jsdelivr.net/gh/Abdulkader-Safi/SafiCSS@v0.1.0/dist/saficss.global.js"></script>Change @v0.1.0 to whatever tag you want. The built files live in dist/ and are committed to the repo, so these URLs work as soon as the tag is pushed. Forking this? Swap Abdulkader-Safi/SafiCSS for your own owner/repo.
For production, pin a version (never @latest) and add Subresource Integrity to the <script> tag: integrity="sha384-..." crossorigin="anonymous". jsDelivr shows the hash for any file on its page.
Returns a class name. Injects the rules once, keyed by a hash of the object.
const cls = css({
color: "white",
padding: 16,
"&:hover": { color: "cyan" },
span: { fontWeight: 700 },
"@media (max-width: 600px)": { padding: 8 },
});
el.className = cls;Key rules:
- Keys are CSS properties in camelCase (
backgroundColor) or kebab strings ("background-color"). Both work. - String and number values become declarations. A number gets
px, except for the unitless properties React treats specially (opacity,zIndex,fontWeight,lineHeight,flex,order,gridColumn, and the rest). Custom properties (--foo) never get a unit. - An object value is a nested block. A key with
&is replaced by the generated class ("&:hover","& > a"). A key starting with@is an at-rule (@media,@supports). Any other key is nested under the class ("span"becomes.saficss-xxx span). - An array value sets fallbacks for one property (
display: ["grid", "flex"]).
Injects an @keyframes block and returns the animation name. Use it in an animation value.
const spin = keyframes({
from: { transform: "rotate(0deg)" },
to: { transform: "rotate(360deg)" },
});
el.className = css({ animation: `${spin} 1s linear infinite` });Writes unscoped global rules: resets, :root custom properties, body styles. Top-level keys are selectors.
injectGlobal({
":root": { "--accent": "#22d3ee" },
body: { margin: 0, fontFamily: "system-ui" },
"*": { boxSizing: "border-box" },
});Sets CSS custom properties at runtime. Defaults to :root; pass an element to scope them. Define var(--accent) in your styles, then call setVars to change it live. Everything using the variable updates with no re-injection.
setVars({ "--accent": "#f00" }); // on :root
setVars({ accent: "blue" }, myEl); // scoped, "--" added for youThe default export bundles all four functions, which is what the global build exposes as window.SafiCSS.
import SafiCSS from "saficss";
SafiCSS.css({ color: "red" });The style object is fully typed on top of csstype, so every standard CSS property autocompletes and a bad value is a type error:
css({ borderColor: 42 }); // Error: number is not assignable to a color
css({ colr: "red" }); // Error: unknown property (typo caught)That is the linting for anyone using SafiCSS. No stylelint needed for the object API. TypeScript is the linter here.
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run test # vitest
npm run build # tsup, writes dist/
npm run format # prettier --writeMIT, Abdulkader Safi.