@xkdkw/css-in-js
v0.1.0
Published
A small CSS-in-JS engine: plain JS objects in, deduped atomic class names and injected <style> rules out.
Readme
css-in-js
A small CSS-in-JS engine: plain JS objects in, deduped atomic class names and
injected <style> rules out. No build step, no plugins, no theming system —
just css, keyframes, and injectGlobal.
css(styles)
import { css } from '@xkdkw/css-in-js';
const button = css({
padding: 8,
borderRadius: 4,
fontWeight: 600,
'&:hover': { opacity: 0.8 },
'@media (min-width: 600px)': { padding: 16 },
});
el.className = button; // e.g. "d1jqtvw2 h13x5f8t mml89ng"Class names are prefixed by bucket, using Griffel's own bucket letters
(github.com/microsoft/griffel's CSSRulesByBucket): d for plain
declarations, l/v/w/f/i/h/a for :link/:visited/
:focus-within/:focus/:focus-visible/:hover/:active respectively,
m for @media, x for @container, t for @supports/@layer/other
at-rules. r (reset) and s (at-rules for reset) exist for ordering but are
never written to — there's no "this is a reset-tier style" signal in a plain
style object. Anything nested that isn't one of those seven curated pseudo-
classes (other pseudo-classes, pseudo-elements, descendant/child
combinators) falls back to d.
Atomic, like Griffel: css() returns one class per declaration, not
one class for the whole object. A padding: 8 used in two completely
unrelated css()/makeStyles() calls anywhere in the app resolves to the
exact same class and the exact same injected rule — real dedup at the
property level, not just "this object was already seen." (See atomic.ts.)
- camelCase properties become kebab-case (
backgroundColor→background-color). - Numbers get
pxappended, except on unitless properties (opacity,zIndex,flex,lineHeight,fontWeight, etc.). - A key starting with
&nests as a compound selector relative to the generated class (&:hover,& > span), same convention as Sass/Emotion. - A key starting with
@wraps the current selector in an at-rule (@media,@supports). - Cascade order is correct regardless of injection order: all 15 buckets
are created together, in Griffel's fixed order, the first time any atom is
injected — so e.g.
:hoverreliably beats:link/:visited,:activebeats:hover, and:focus-visiblebeats plain:focus, regardless of which rule happened to be reached first at runtime. Equal-specificity cascade ties resolve by document order, which is why the buckets' fixed relative position — not injection order — is what makes this correct. - Calling
css()again with an equivalent object (compared by value, not reference, key order doesn't matter) returns the identical class string.
keyframes(steps)
import { keyframes, css } from '@xkdkw/css-in-js';
const spin = keyframes({
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
});
const spinner = css({ animationName: spin, animationDuration: '1s' });injectGlobal(rules)
For real selectors, not generated classes — resets and the like:
import { injectGlobal } from '@xkdkw/css-in-js';
injectGlobal({
body: { margin: 0, fontFamily: 'sans-serif' },
'*': { boxSizing: 'border-box' },
});makeStyles(slots) / mergeClasses(...)
Griffel's calling convention — a slot-keyed style map in, a cheap useStyles()
accessor out. Each slot compiles through the same atomic css(), so two
different slots (even in different makeStyles() calls, different
components, anywhere in the app) that happen to share one declaration share
the same underlying atomic class for it too.
import { makeStyles, mergeClasses } from '@xkdkw/css-in-js';
const useStyles = makeStyles({
root: { display: 'flex', padding: 8 },
icon: { color: 'red', '&:hover': { color: 'blue' } },
});
function Button({ className, active }: { className?: string; active?: boolean }) {
const styles = useStyles(); // cheap after the first call — memoized
return el('button', {
class: mergeClasses(styles.root, active && styles.icon, className),
});
}useStyles() computes its slot classes once (on first call) and returns the
same object every time after — calling it on every render is intentionally
cheap, matching Griffel's useStyles(). mergeClasses just joins its
arguments with a space, dropping falsy ones, for combining slots with
conditional or externally-passed class names.
makeStaticStyles(rules)
Griffel's global-styles counterpart to makeStyles — same selector-keyed
shape as injectGlobal, but returned as a hook-shaped accessor meant to be
called from a component (typically the app root, as useStaticStyles()) on
every render, cheaply:
import { makeStaticStyles } from '@xkdkw/css-in-js';
const useStaticStyles = makeStaticStyles({
body: { margin: 0, fontFamily: 'sans-serif' },
'*': { boxSizing: 'border-box' },
});
function App() {
useStaticStyles(); // safe to call on every render — injects once
return el('div', null, /* ... */);
}Built on injectGlobal() under the hood; the wrapper just avoids
re-stringifying/re-hashing the same rules on every call only to discover
they're already injected — same "compute once" shape as makeStyles's
useStyles().
Consuming it
npm install @xkdkw/css-in-js