@atmc/core
Atomic CSS-in-JS with a featherweight runtime.
Usage
Introduction through examples
The code snippets below are deliberately framework-agnostic. Please refer to the project's repository for information about integrations.
Basic concepts of atomicity
import { css } from "@atmc/core";
document.querySelector("#element-1").className = css({ color: "red" });
document.querySelector("#element-2").className = css({ color: "blue" });
console.assert(css({ color: "red" }) === css({ color: " red " }));
document.querySelector("#element-3").className = css({
color: "red",
":hover": {
color: "blue"
}
});
Numbers assigned to non-unitless properties are postfixed with "px"
document.querySelector("#element-4").className = css({
padding: 8,
lineHeight: 1.5
});
At-rules like media queries can be used and combined with pseudos
document.querySelector("#element-5").className = css({
"@media": {
"(min-width: 600px)": {
color: "rebeccapurple",
":hover": {
background: "papayawhip"
}
},
"(min-width: 1000px)": {
color: "teal"
}
}
});
Fallback values are accepted when auto-prefixing isn't enough
document.querySelector("#element-6").className = css({
display: "flex",
justifyContent: ["space-around", "space-evenly"]
});
Using keyframes to animate values of given properties over time
const pulse = keyframes({
from: { opacity: 0 },
to: { opacity: 1 }
});
const className = css({
animation: `${pulse} 3s infinite alternate`
});
Advanced selectors may be used as an escape hatch from strict atomicity
const className = css({
display: "flex",
selectors: {
"& > * + *": {
marginLeft: 16
},
"&:focus, &:active": {
outline: "solid"
}
}
});
Server-side rendering
While prerendering a page, browser object models are inaccessible and thus, styles cannot be injected dynamically. However, a VirtualInjector
can collect the styles instead of applying them through injection, as seen in the Next.js example:
import { setup } from "@atmc/core";
import {
filterOutUnusedRules,
getStyleTag,
VirtualInjector
} from "@atmc/core/server";
export const sharedOptions = {};
const injector = VirtualInjector();
setup({ ...sharedOptions, injector });
let html = renderToString(element);
const styleTag = getStyleTag(filterOutUnusedRules(injector, page.html));
html = html.replace("</head>", styleTag + "</head>");
During runtime, the same options should be provided before hydration, as shown below:
import { hydrate, setup } from "@atmc/core";
import { sharedOptions } from "./server";
if (typeof window !== "undefined") {
setup(options);
hydrate();
}
Deno support
For convenient resolution of the library, an import map should be used. Unlike with Node, development and production builds are separated into different bundles.
/* import_map.json */
{
"imports": {
"@atmc/core/dev": "https://cdn.pika.dev/@atmc/core@X.Y.Z/runtime-deno-dev",
"@atmc/core": "https://cdn.pika.dev/@atmc/core@X.Y.Z/runtime-deno"
}
}
deno run --importmap=import_map.json --unstable mod.ts
Security
User-specified data shall be escaped manually using CSS.escape()
or an equivalent method.
Customization
Injector options
nonce
In order to prevent harmful code injection on the web, a Content Security Policy (CSP) may be put in place. During server-side rendering, a cryptographic nonce (number used once) may be embedded when generating a page on demand:
import { VirtualInjector } from "@atmc/core/server";
const injector = VirtualInjector({ nonce: __webpack_nonce__ });
The same nonce
parameter should be supplied to the client-side injector:
import { CSSOMInjector, DOMInjector, setup } from "@atmc/core";
const isDev = process.env.NODE_ENV !== "production";
setup({
injector: isDev
? DOMInjector({ nonce: __webpack_nonce__ })
: CSSOMInjector({ nonce: __webpack_nonce__ })
});
target
Changes the destination of the injected rules. By default, a <style id="__@atmc/core">
element in the <head>
during runtime, which gets created if unavailable.
Instance options
prefix
A custom auto-prefixer method may be used as a replacement for the built-in tiny-css-prefixer
:
import { setup } from "@atmc/core";
import { prefix as stylisPrefix } from "stylis";
setup({
prefix: (property, value) => {
const declaration = `${property}:${value};`;
return (
stylisPrefix(declaration, property.length).slice(0, -1)
);
}
});
Instance creation
Separate instances of @atmc/core are necessary when managing styles of multiple browsing contexts (e.g. an <iframe>
besides the main document). This option should be used along with a custom target
for injection:
import { createInstance, CSSOMInjector } from "@atmc/core";
const iframeDocument = document.getElementsByTagName("iframe")[0]
.contentDocument;
export const instance = createInstance();
instance.setup({
injector: CSSOMInjector({
target: iframeDocument.getElementById("@atmc/core")
})
});
What's missing
Global styles
Being unique by nature, non-scoped styles should not be decomposed into atomic rules. This library doesn't support injecting global styles, as they may cause unexpected side-effects. However, ordinary CSS can still be used for style sheet normalization and defining the values of CSS Custom Properties.
Contrary to @atmc/core-managed styles, CSS referenced from a <link>
tag may persist in the cache during page changes. Global styles are suitable for application-wide styling (e.g. normalization/reset), while inlining the scoped rules generated by @atmc/core accounts for faster page transitions due to the varying nature of per-page styles.
By omitting global styling functionality on purpose, @atmc/core can maintain its low bundle footprint while also encouraging performance-focused development patterns.
Theming
Many CSS-in-JS libraries tend to ship their own theming solutions. Contrary to others, @atmc/core doesn't embrace a single recommended method, leaving more choices for developers. Concepts below are encouraged: