Skip to content

Instantly share code, notes, and snippets.

@mattmccray
Last active April 26, 2024 10:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattmccray/97c41d4d801fc8283f0984e038facebf to your computer and use it in GitHub Desktop.
Save mattmccray/97c41d4d801fc8283f0984e038facebf to your computer and use it in GitHub Desktop.
css.js -- Small, inline css for components

css.js

A CSS in JS micro-tool that's < 1kb (gzipped).

Usage:

const MyComponent = (props) => {
  return (
    <div className={myCompStyles({ hasError: props.error })}>
      This is a test.
    </div>
  )
}

const myCompStyles = css`
  & {
    padding: 6px 12px;
    background: dodgerblue;
    color: white;
  }
  
  &.hasError {
    background: crimson;
  }
`
interface IConfiguration {
append: "each" | "batch";
debug: boolean;
target: Element;
regExp: RegExp;
hashIds: boolean;
prefix: string;
}
/**
* Configures the css function. Useful if you want a different "this" character
* (defaults to &) or if you want to enable debug mode or caclulate hashes for
* class names instead of using date based ids.
*/
declare function configure(options: IConfiguration): void;
/**
* Tagged template literal that accepts css and inserts it into the DOM,
* returning a generated class name.
*/
declare function cssRules(template: TemplateStringsArray, ...params: any[]): string;
/**
* Assembles a class name from the given parameters. Strings are added as-is,
* object keys are added if their values are truthy. Null and undefined
* values are ignored.
*/
declare function classNames(...extra: (string | Partial<Record<string, any>> | undefined | null)[]): string;
type classNameTypes = string | Partial<Record<string, any>> | undefined | null;
declare function classBuilder<T extends string>(id: string): {
(...extra: classNameTypes[]): string;
valueOf(): string;
toString(): string;
$: (...extra: classNameTypes[]) => string;
inner(...extra: classNameTypes[]): string;
};
/**
* Tagged template literal function that returns a function that can be used
* to assembly class names prefixed with the generated one from the css template.
*/
declare function css(template: TemplateStringsArray, ...params: any[]): ReturnType<typeof classBuilder>;
declare namespace css {
var config: typeof configure;
}
declare function uid(radix?: number): string;
/**
* A string hashing function based on Daniel J. Bernstein's popular 'times 33' hash algorithm.
*/
declare function crcHash(text: string): number;
export { classNames, configure, crcHash, css, cssRules, css as default, uid };
// esm
var a=0,l=null,s={debug:!1,append:"batch",target:document.head,regExp:/&/g,hashIds:!1,prefix:"css"},d=((e=new Date)=>new Date(e.getFullYear(),e.getMonth()).getTime())(),h=(()=>{try{var e=new CSSStyleSheet;return"adoptedStyleSheets"in document&&"replace"in e}catch{return!1}})();function S(e){s=Object.assign({},s,e)}function y(e,...t){let n="";for(let i=0;i<t.length;i++)n+=String(e[i]),n+=String(t[i]);e.length>t.length&&(n+=e[e.length-1]);let r=C(n);return n=c(n,`.${r}`),s.debug&&(console.trace(s.append,n),E(n)),x(n),r}function g(...e){let t="";for(let n=0;n<e.length;n++){let r=e[n];if(r)if(typeof r=="string")t+=r+" ";else{let i=Object.keys(r);for(let o=0;o<i.length;o++){let u=i[o];r[u]&&(t+=u+" ")}}else continue}return t}function m(e){function t(...n){return c(g(e,...n),e)}return t.valueOf=()=>e,t.toString=()=>e,t.$=t.inner=(...n)=>c(g(...n),e),t}function p(e,...t){return m(y(e,...t))}p.config=S;var _=p;function T(e=36){let t=Date.now()-d;return t<=a&&(t=a+1),a=t,a.toString(e)}function b(e){"use strict";for(var t=5381,n=e.length;n;)t=t*33^e.charCodeAt(--n);return t>>>0}function c(e,t){return e.replace(s.regExp,t)}function x(e,t=()=>document.createElement("style")){s.append=="each"?f(e,t):l===null?(l=e,setTimeout(()=>{f(l,t),l=null},0)):l=l+`
`+e}function C(e){let t=s.hashIds?b(e):T();return`${s.prefix}_${t}`}function f(e,t){if(h&&!s.debug){let r=new CSSStyleSheet;r.replace(e),document.adoptedStyleSheets=[...document.adoptedStyleSheets,r];return}let n=t();n.setAttribute("type","text/css"),n.innerHTML=e,s.target.appendChild(n),s.debug&&console.trace({styles:e})}function E(e){let t=0,n=0;for(let r=0;r<e.length;r++)e[r]==="{"?t++:e[r]==="}"&&n++;if(t!==n)throw new Error("Invalid CSS: Bracket mismatch.")}export{g as classNames,S as configure,b as crcHash,p as css,y as cssRules,_ as default,T as uid};
// ./node_modules/.bin/tsup css.ts --dts --format esm,cjs,iife --minify
// gzip -c dist/css.js | wc -c
// => 992
interface IConfiguration {
append: "each" | "batch"
debug: boolean
target: Element
regExp: RegExp
hashIds: boolean
prefix: string
}
let _prevUid = 0
let _pendingStyles: string | null = null
let _config: IConfiguration = {
debug: false,
append: "batch",
target: document.head,
regExp: /&/g,
hashIds: false,
prefix: "css",
}
// const MINI_EPOCH = 1690866000000
const MINI_EPOCH = ((now = new Date()) => {
return new Date(now.getFullYear(), now.getMonth()).getTime()
})()
const SUPPORTS_CONSTRUCTABLE_STYLESHEETS = (() => {
try {
var sheet = new CSSStyleSheet()
return "adoptedStyleSheets" in document && "replace" in sheet
} catch (err) {
return false
}
})()
/**
* Configures the css function. Useful if you want a different "this" character
* (defaults to &) or if you want to enable debug mode or caclulate hashes for
* class names instead of using date based ids.
*/
export function configure(options: IConfiguration) {
_config = Object.assign({}, _config, options)
}
/**
* Tagged template literal that accepts css and inserts it into the DOM,
* returning a generated class name.
*/
export function cssRules(
template: TemplateStringsArray,
...params: any[]
): string {
let styles = ""
for (let i = 0; i < params.length; i++) {
styles += String(template[i])
styles += String(params[i])
}
if (template.length > params.length) {
styles += template[template.length - 1]
}
const clsName = _createClassName(styles)
styles = _substituteClassname(styles, `.${clsName}`)
if (_config.debug) {
console.trace(_config.append, styles)
assertValidCSS(styles)
}
_appendStyles(styles)
return clsName
}
/**
* Assembles a class name from the given parameters. Strings are added as-is,
* object keys are added if their values are truthy. Null and undefined
* values are ignored.
*/
export function classNames(
...extra: (string | Partial<Record<string, any>> | undefined | null)[]
) {
let className = ""
for (let i = 0; i < extra.length; i++) {
const item = extra[i]
if (!item) continue
else if (typeof item === "string") {
className += item + " "
} else {
const keys = Object.keys(item)
for (let j = 0; j < keys.length; j++) {
const key = keys[j]
if (item[key]) {
className += key + " "
}
}
}
}
return className
}
type classNameTypes = string | Partial<Record<string, any>> | undefined | null
function classBuilder<T extends string>(id: string) {
function apply(...extra: classNameTypes[]) {
return _substituteClassname(classNames(id, ...extra), id)
}
apply.valueOf = () => id
apply.toString = () => id
apply.$ = apply.inner = (...extra: classNameTypes[]) =>
_substituteClassname(classNames(...extra), id)
return apply
}
/**
* Tagged template literal function that returns a function that can be used
* to assembly class names prefixed with the generated one from the css template.
*/
export function css(
template: TemplateStringsArray,
...params: any[]
): ReturnType<typeof classBuilder> {
return classBuilder(cssRules(template, ...params))
}
css.config = configure
export default css
export function uid(radix: number = 36): string {
let now = Date.now() - MINI_EPOCH
if (now <= _prevUid) now = _prevUid + 1
_prevUid = now
return _prevUid.toString(radix)
}
/**
* A string hashing function based on Daniel J. Bernstein's popular 'times 33' hash algorithm.
*/
export function crcHash(text: string): number {
"use strict"
var hash = 5381
var index = text.length
while (index) {
hash = (hash * 33) ^ text.charCodeAt(--index)
}
return hash >>> 0
}
function _substituteClassname(source: string, className: string): string {
return source.replace(_config.regExp, className)
}
function _appendStyles(
styles: string,
createElem: () => HTMLStyleElement = () => document.createElement("style")
) {
if (_config.append == "each") {
_createAndAppendStyleElement(styles, createElem)
} else {
if (_pendingStyles === null) {
_pendingStyles = styles
setTimeout(() => {
_createAndAppendStyleElement(_pendingStyles!, createElem)
_pendingStyles = null
}, 0)
} else {
_pendingStyles = _pendingStyles + `\n` + styles
}
}
}
function _createClassName(styles: string) {
const blockId = _config.hashIds ? crcHash(styles) : uid()
return `${_config.prefix}_${blockId}`
}
function _createAndAppendStyleElement(
styles: string,
createElem: () => HTMLStyleElement
) {
if (SUPPORTS_CONSTRUCTABLE_STYLESHEETS && !_config.debug) {
const stylesheet: any = new CSSStyleSheet()
stylesheet.replace(styles)
//@ts-ignore
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet]
return
}
const target = createElem()
target.setAttribute("type", "text/css")
// target.setAttribute("id", `css_${uid()}`)
target.innerHTML = styles
_config.target.appendChild(target)
if (_config.debug) console.trace({ styles })
}
function assertValidCSS(styles: string) {
// Validate that the number of closing brackets matches the number of opening brackets
let openBrackets = 0
let closeBrackets = 0
for (let i = 0; i < styles.length; i++) {
if (styles[i] === "{") openBrackets++
else if (styles[i] === "}") closeBrackets++
}
if (openBrackets !== closeBrackets) {
throw new Error("Invalid CSS: Bracket mismatch.")
}
}
@mattmccray
Copy link
Author

Bonus!

If you use React, you can create a simple styled component like thing like this:

// Add the following to css.ts

import React from "react"

interface ICssComponent<T extends keyof JSX.IntrinsicElements> {
  (props: JSX.IntrinsicElements[T]): JSX.Element
  classNames: ReturnType<typeof classBuilder>
}

export function component(styles: string): ICssComponent<"div">
export function component<T extends keyof JSX.IntrinsicElements>(
  tag: T | string | ICssComponent<any>,
  styles: string
): ICssComponent<T>
export function component<T extends keyof JSX.IntrinsicElements>(
  tag?: string | T | ICssComponent<any>,
  styles?: string
): ICssComponent<any> {
  let extraClassNames = ""

  if (!styles) {
    if (typeof tag !== "string") throw new Error("No styles provided")
    styles = tag
    tag = "div"
  }
  if (typeof tag === "string" && tag.includes(".")) {
    const parts = tag.split(".")
    tag = parts.shift()
    extraClassNames = parts.join(" ")
  }
  function CssComponent(props: any) {
    const { className, children } = props
    const combinedClassName = `${styles} ${className || ""} ${extraClassNames}`
    return React.createElement(
      tag!,
      {
        className: combinedClassName,
        ...props,
      },
      children
    )
  }
  CssComponent.classNames = classBuilder(styles, false)

  return CssComponent
}

Usage:

const ActionListRoot = component(cssRules`
  & {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-gap: 12px;
  }
`)

// Elsewhere, should type-complete as a div
<ActionListRoot />

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment