Skip to content

Instantly share code, notes, and snippets.

@dead-claudia
Last active May 1, 2019 18:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dead-claudia/7f3757622e0cd6d9ac9fbadc673e0695 to your computer and use it in GitHub Desktop.
Save dead-claudia/7f3757622e0cd6d9ac9fbadc673e0695 to your computer and use it in GitHub Desktop.
Mini CSS-in-JS framework in under 50 lines of ES6 code

A mini CSS-in-JS framework

It exports two functions:

  • renderToString(styles) - Render the CSS to a string. (This has no DOM dependency, and it omits the @charset - that's for you to set.)
  • renderToDOM(styles) - Render the CSS to the DOM.

Note that renderToString omits any charset - the user should specify that before emitting.

Styles are specified as follows:

const styles = {
    // Optional: provide parent styles
    "@extend": [...parents],
    // Optional: provide imports with conditions
    "@import": [
        "url",
        ["url", "screen"],
        ["url", "supports(display: flex)"],
    ],
    // Optional: provide namespaces (usually useless)
    "@namespace": {
        // Specify root namespace
        "": "http://www.w3.org/1999/xhtml",
        // Specify namespace for a particular prefix
        "svg": "http://www.w3.org/2000/svg",
    },

    // Define selectors with properties
    ".foo": {
        display: "flex",

        // Define nested selectors
        "> .bar": {
            "margin": "0 auto",
        }
    },

    // Define at-rules
    "@page": {
        color: "red",
        "@first": {color: "green"},
    },
}

Note that this doesn't support nested at-rules like ".foo": {"@media screen": ...}.

export function renderToString(styles) {
const entries = Object.entries || o => Object.keys(o).map(k => [k, o[k]])
const uniq = list => [...new Set(list)]
const stringify = key => `"${key
.replace(/[\\"]/g, "\\$1")
.replace(/\0/g, "\uFFFD")
.replace(/[\x01-\x1f\x7f]/g, (m) => `\\${m.toString(16)} `)
}"`
const resolve = styles => [].concat(
...(styles["@extend"] || []).filter(s => s != null).map(resolve),
[styles]
)
const render = (prefix, current) => uniq([].concat(
...entries(current).filter(([key, value]) =>
!/^@(namespace|import|extend)/.test(key) &&
value != null && value !== ""
).map(([key, value]) => typeof value === "object"
? render(key, value)
: ["", "-o-", "-ms-", "-webkit-", "-moz-"]
.map(p => `${prefix}{${p}${key}:${value}}`)
.join("")
)
))
styles = resolve(styles).reduce(
(target, source) => ({
...target, ...source,
"@namespace": {...target["@namespace"], ...source["@namespace"]},
"@import": uniq([...target["@import"], ...source["@import"] || []]),
}),
{"@namespace": {}, "@import": []}
)
return [
...entries(styles["@namespace"] || {})
.map(([key, url]) => `@namespace ${key}${stringify(url)};`)
[...styles["@imports"] || []]
.map(value => Array.isArray(value) ? value : [value])
.map(([source, query]) => `@import${stringify(source)}${query};`)
...render("", styles),
].join("")
}
export function renderToDOM(styles) {
const css = `@charset'utf-8';${renderToString(styles)}`
const style = document.createElement("style")
style.type = "text/css"
if (style.styleSheet) style.styleSheet.cssText = css
else style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment